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

@@ -1,2 +1,5 @@
* Neue Einstellung um Animationen zu deaktivieren
* Verbesserte Validierung von Server-Adressen
* Beheben von Fehlern, durch welche es zu Problemen mit Paperless-ngx 2.x.x kam
* Weitere, kleinere Fehlerbehebungen
* Neue Übersetzungen

View File

@@ -1,2 +1,5 @@
* Add setting to disable animations
* Improved server-address validation
* Fixed a bug which caused issues with newer versions of Paperless-ngx (2.x.x)
* Minor bugfixes
* Updated translations

View File

@@ -1 +1 @@
../android/fastlane/metadata
../../android/fastlane/metadata

Submodule flutter deleted from d211f42860

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")) {
} 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),
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());
}
}
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,31 +1,46 @@
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();
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);
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
// UserRepositoryState? fromJson(Map<String, dynamic> json) {

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

View File

@@ -3,30 +3,23 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_bulk_action_state.dart';
part 'document_bulk_action_cubit.freezed.dart';
class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
final PaperlessDocumentsApi _documentsApi;
final LabelRepository _labelRepository;
final DocumentChangedNotifier _notifier;
DocumentBulkActionCubit(
this._documentsApi,
this._labelRepository,
this._notifier, {
required List<DocumentModel> selection,
}) : super(
DocumentBulkActionState(
selection: selection,
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
),
) {
_notifier.addListener(
@@ -42,19 +35,6 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
);
},
);
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
),
);
},
);
}
Future<void> bulkDelete() async {
@@ -69,6 +49,7 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
}
Future<void> bulkModifyCorrespondent(int? correspondentId) async {
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.correspondent(
state.selectedIds,
@@ -81,9 +62,18 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
}
}
Future<void> bulkModifyDocumentType(int? documentTypeId) async {
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.documentType(
state.selectedIds,
@@ -96,9 +86,18 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
}
}
Future<void> bulkModifyStoragePath(int? storagePathId) async {
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.storagePath(
state.selectedIds,
@@ -111,12 +110,21 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
}
}
Future<void> bulkModifyTags({
Iterable<int> addTagIds = const [],
Iterable<int> removeTagIds = const [],
}) async {
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyTagsAction(
state.selectedIds,
@@ -133,12 +141,19 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
}
}
@override
Future<void> close() {
_notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close();
}
}

View File

@@ -1,15 +1,18 @@
part of 'document_bulk_action_cubit.dart';
@freezed
class DocumentBulkActionState with _$DocumentBulkActionState {
const DocumentBulkActionState._();
const factory DocumentBulkActionState({
required List<DocumentModel> selection,
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _DocumentBulkActionState;
class DocumentBulkActionState {
final List<DocumentModel> selection;
DocumentBulkActionState({
required this.selection,
});
Iterable<int> get selectedIds => selection.map((d) => d.id);
DocumentBulkActionState copyWith({
List<DocumentModel>? selection,
}) {
return DocumentBulkActionState(
selection: selection ?? this.selection,
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
@@ -35,11 +36,12 @@ class _FullscreenBulkEditTagsWidgetState
void initState() {
super.initState();
final state = context.read<DocumentBulkActionCubit>().state;
final labels = context.read<LabelRepository>();
_sharedTags = state.selection
.map((e) => e.tags)
.map((e) => e.toSet())
.fold(
state.tags.values.map((e) => e.id!).toSet(),
labels.tags.values.map((e) => e.id!).toSet(),
(previousValue, element) => previousValue.intersection(element),
)
.toList();
@@ -49,14 +51,10 @@ class _FullscreenBulkEditTagsWidgetState
.toSet()
.difference(_sharedTags.toSet())
.toList();
_filteredTags = state.tags.keys.toList();
_filteredTags = labels.tags.keys.toList();
_controller.addListener(() {
setState(() {
_filteredTags = context
.read<DocumentBulkActionCubit>()
.state
.tags
.values
_filteredTags = labels.tags.values
.where((e) =>
e.name.normalized().contains(_controller.text.normalized()))
.map((e) => e.id!)
@@ -69,6 +67,7 @@ class _FullscreenBulkEditTagsWidgetState
@override
Widget build(BuildContext context) {
final labelRepository = context.watch<LabelRepository>();
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
return FullscreenSelectionForm(
@@ -86,7 +85,7 @@ class _FullscreenBulkEditTagsWidgetState
selectionBuilder: (context, index) {
return _buildTagOption(
_filteredTags[index],
state.tags,
labelRepository.tags,
);
},
selectionCount: _filteredTags.length,
@@ -155,11 +154,12 @@ class _FullscreenBulkEditTagsWidgetState
void _submit() async {
if (_addTags.isNotEmpty || _removeTags.isNotEmpty) {
final bloc = context.read<DocumentBulkActionCubit>();
final labelRepository = context.read<LabelRepository>();
final addNames = _addTags
.map((value) => "\"${bloc.state.tags[value]!.name}\"")
.map((value) => "\"${labelRepository.tags[value]!.name}\"")
.toList();
final removeNames = _removeTags
.map((value) => "\"${bloc.state.tags[value]!.name}\"")
.map((value) => "\"${labelRepository.tags[value]!.name}\"")
.toList();
final shouldPerformAction = await showDialog<bool>(
context: context,

View File

@@ -4,8 +4,11 @@ import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/loading_status.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
@@ -15,6 +18,7 @@ import 'package:path/path.dart' as p;
import 'package:printing/printing.dart';
import 'package:share_plus/share_plus.dart';
part 'document_details_cubit.freezed.dart';
part 'document_details_state.dart';
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
@@ -22,15 +26,13 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final PaperlessDocumentsApi _api;
final DocumentChangedNotifier _notifier;
final LocalNotificationService _notificationService;
final LabelRepository _labelRepository;
DocumentDetailsCubit(
this._api,
this._labelRepository,
this._notifier,
this._notificationService, {
required this.id,
}) : super(const DocumentDetailsInitial()) {
}) : super(const DocumentDetailsState()) {
_notifier.addListener(
this,
onUpdated: (document) {
@@ -42,7 +44,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<void> initialize() async {
debugPrint("Initialize called");
emit(const DocumentDetailsLoading());
emit(const DocumentDetailsState(status: LoadingStatus.loading));
try {
final (document, metaData) = await Future.wait([
_api.find(id),
@@ -54,11 +56,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
// final document = await _api.find(id);
// final metaData = await _api.getMetaData(id);
debugPrint("Document data loaded for $id");
emit(DocumentDetailsLoaded(
emit(DocumentDetailsState(
status: LoadingStatus.loaded,
document: document,
metaData: metaData,
));
} catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
logger.fe(
"An error occurred while loading data for document $id.",
className: runtimeType.toString(),
@@ -66,13 +69,22 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
error: error,
stackTrace: stackTrace,
);
emit(const DocumentDetailsError());
emit(const DocumentDetailsState(status: LoadingStatus.error));
addError(
TransientPaperlessApiError(code: error.code, details: error.details),
);
}
}
Future<void> delete(DocumentModel document) async {
try {
await _api.delete(document);
_notifier.notifyDeleted(document);
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(code: e.code, details: e.details),
);
}
}
Future<void> assignAsn(
@@ -80,6 +92,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
int? asn,
bool autoAssign = false,
}) async {
try {
if (!autoAssign) {
final updatedDocument = await _api.update(
document.copyWith(archiveSerialNumber: () => asn),
@@ -91,18 +104,22 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
.update(document.copyWith(archiveSerialNumber: () => autoAsn));
_notifier.notifyUpdated(updatedDocument);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(code: e.code, details: e.details),
);
}
}
Future<ResultType> openDocumentInSystemViewer() async {
final s = state;
if (s is! DocumentDetailsLoaded) {
if (state.status != LoadingStatus.loaded) {
throw Exception(
"Document cannot be opened in system viewer "
"if document information has not yet been loaded.",
);
}
final cacheDir = FileService.instance.temporaryDirectory;
final filePath = s.metaData.mediaFilename.replaceAll("/", " ");
final filePath = state.metaData!.mediaFilename.replaceAll("/", " ");
final fileName = "${p.basenameWithoutExtension(filePath)}.pdf";
final file = File("${cacheDir.path}/$fileName");
@@ -110,7 +127,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (!file.existsSync()) {
file.createSync();
await _api.downloadToFile(
s.document,
state.document!,
file.path,
);
}
@@ -121,14 +138,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
}
void replace(DocumentModel document) {
final s = state;
if (s is! DocumentDetailsLoaded) {
return;
}
emit(DocumentDetailsLoaded(
document: document,
metaData: s.metaData,
));
emit(state.copyWith(document: document));
}
Future<void> downloadDocument({
@@ -136,12 +146,11 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
required String locale,
required String userId,
}) async {
final s = state;
if (s is! DocumentDetailsLoaded) {
if (state.status != LoadingStatus.loaded) {
return;
}
String targetPath = _buildDownloadFilePath(
s.metaData,
state.metaData!,
downloadOriginal,
FileService.instance.downloadsDirectory,
);
@@ -150,7 +159,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
await File(targetPath).create();
} else {
await _notificationService.notifyDocumentDownload(
document: s.document,
document: state.document!,
filename: p.basename(targetPath),
filePath: targetPath,
finished: true,
@@ -169,12 +178,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
// );
await _api.downloadToFile(
s.document,
state.document!,
targetPath,
original: downloadOriginal,
onProgressChanged: (progress) {
_notificationService.notifyDocumentDownload(
document: s.document,
document: state.document!,
filename: p.basename(targetPath),
filePath: targetPath,
finished: true,
@@ -185,28 +194,27 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
},
);
await _notificationService.notifyDocumentDownload(
document: s.document,
document: state.document!,
filename: p.basename(targetPath),
filePath: targetPath,
finished: true,
locale: locale,
userId: userId,
);
logger.fi("Document '${s.document.title}' saved to $targetPath.");
logger.fi("Document '${state.document!.title}' saved to $targetPath.");
}
Future<void> shareDocument({bool shareOriginal = false}) async {
final s = state;
if (s is! DocumentDetailsLoaded) {
if (state.status != LoadingStatus.loaded) {
return;
}
String filePath = _buildDownloadFilePath(
s.metaData,
state.metaData!,
shareOriginal,
FileService.instance.temporaryDirectory,
);
await _api.downloadToFile(
s.document,
state.document!,
filePath,
original: shareOriginal,
);
@@ -214,27 +222,26 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
[
XFile(
filePath,
name: s.document.originalFileName,
name: state.document!.originalFileName,
mimeType: "application/pdf",
lastModified: s.document.modified,
lastModified: state.document!.modified,
),
],
subject: s.document.title,
subject: state.document!.title,
);
}
Future<void> printDocument() async {
final s = state;
if (s is! DocumentDetailsLoaded) {
if (state.status != LoadingStatus.loaded) {
return;
}
final filePath = _buildDownloadFilePath(
s.metaData,
state.metaData!,
false,
FileService.instance.temporaryDirectory,
);
await _api.downloadToFile(
s.document,
state.document!,
filePath,
original: false,
);
@@ -243,13 +250,16 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
throw Exception("An error occurred while downloading the document.");
}
Printing.layoutPdf(
name: s.document.title,
name: state.document!.title,
onLayout: (format) => file.readAsBytesSync(),
);
}
String _buildDownloadFilePath(
DocumentMetaData meta, bool original, Directory dir) {
DocumentMetaData meta,
bool original,
Directory dir,
) {
final normalizedPath = meta.mediaFilename.replaceAll("/", " ");
final extension = original ? p.extension(normalizedPath) : '.pdf';
return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension";
@@ -257,7 +267,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
@override
Future<void> close() async {
_labelRepository.removeListener(this);
_notifier.removeListener(this);
await super.close();
}

View File

@@ -1,41 +1,10 @@
part of 'document_details_cubit.dart';
sealed class DocumentDetailsState {
const DocumentDetailsState();
@freezed
class DocumentDetailsState with _$DocumentDetailsState {
const factory DocumentDetailsState({
@Default(LoadingStatus.initial) LoadingStatus status,
DocumentModel? document,
DocumentMetaData? metaData,
}) = _DocumentDetailsState;
}
class DocumentDetailsInitial extends DocumentDetailsState {
const DocumentDetailsInitial();
}
class DocumentDetailsLoading extends DocumentDetailsState {
const DocumentDetailsLoading();
}
class DocumentDetailsLoaded extends DocumentDetailsState {
final DocumentModel document;
final DocumentMetaData metaData;
const DocumentDetailsLoaded({
required this.document,
required this.metaData,
});
}
class DocumentDetailsError extends DocumentDetailsState {
const DocumentDetailsError();
}
// @freezed
// class DocumentDetailsState with _$DocumentDetailsState {
// const factory DocumentDetailsState({
// required DocumentModel document,
// DocumentMetaData? metaData,
// @Default(false) bool isFullContentLoaded,
// @Default({}) Map<int, Correspondent> correspondents,
// @Default({}) Map<int, DocumentType> documentTypes,
// @Default({}) Map<int, Tag> tags,
// @Default({}) Map<int, StoragePath> storagePaths,
// }) = _DocumentDetailsState;
// }

View File

@@ -6,6 +6,7 @@ import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/accessibility/accessibility_utils.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/loading_status.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
@@ -15,6 +16,7 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
@@ -65,7 +67,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
debugPrint(disableAnimations.toString());
final hasMultiUserSupport =
context.watch<LocalUserAccount>().hasMultiUserSupport;
final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0);
final tabLength = 4 + (hasMultiUserSupport ? 1 : 0);
return AnnotatedRegion(
value: buildOverlayStyle(
Theme.of(context),
@@ -79,9 +81,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
extendBodyBehindAppBar: false,
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
floatingActionButton: switch (state) {
DocumentDetailsLoaded(document: var document) =>
_buildEditButton(document),
floatingActionButton: switch (state.status) {
LoadingStatus.loaded => _buildEditButton(state.document!),
_ => null
},
bottomNavigationBar: _buildBottomAppBar(),
@@ -93,9 +94,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
sliver:
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
final title = switch (state) {
DocumentDetailsLoaded(document: var document) =>
document.title,
final title = switch (state.status) {
LoadingStatus.loaded => state.document!.title,
_ => widget.title ?? '',
};
return SliverAppBar(
@@ -201,17 +201,17 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
),
),
// if (hasMultiUserSupport && false)
// Tab(
// child: Text(
// "Permissions",
// style: TextStyle(
// color: Theme.of(context)
// .colorScheme
// .onPrimaryContainer,
// ),
// ),
// ),
if (hasMultiUserSupport)
Tab(
child: Text(
"Permissions",
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
],
),
),
@@ -227,7 +227,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
context.read(),
context.read(),
context.read(),
context.read(),
documentId: widget.id,
),
child: Padding(
@@ -243,17 +242,15 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
switch (state) {
DocumentDetailsLoaded(
document: var document
) =>
switch (state.status) {
LoadingStatus.loaded =>
DocumentOverviewWidget(
document: document,
document: state.document!,
itemSpacing: _itemSpacing,
queryString:
widget.titleAndContentQueryString,
),
DocumentDetailsError() => _buildErrorState(),
LoadingStatus.error => _buildErrorState(),
_ => _buildLoadingState(),
},
],
@@ -264,16 +261,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
switch (state) {
DocumentDetailsLoaded(
document: var document
) =>
DocumentContentWidget(
document: document,
switch (state.status) {
LoadingStatus.loaded => DocumentContentWidget(
document: state.document!,
queryString:
widget.titleAndContentQueryString,
),
DocumentDetailsError() => _buildErrorState(),
LoadingStatus.error => _buildErrorState(),
_ => _buildLoadingState(),
}
],
@@ -284,17 +278,14 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
switch (state) {
DocumentDetailsLoaded(
document: var document,
metaData: var metaData,
) =>
switch (state.status) {
LoadingStatus.loaded =>
DocumentMetaDataWidget(
document: document,
document: state.document!,
itemSpacing: _itemSpacing,
metaData: metaData,
metaData: state.metaData!,
),
DocumentDetailsError() => _buildErrorState(),
LoadingStatus.error => _buildErrorState(),
_ => _buildLoadingState(),
},
],
@@ -312,20 +303,25 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
],
),
// if (hasMultiUserSupport && false)
// CustomScrollView(
// controller: _pagingScrollController,
// slivers: [
// SliverOverlapInjector(
// handle: NestedScrollView
// .sliverOverlapAbsorberHandleFor(
// context),
// ),
// DocumentPermissionsWidget(
// document: state.document,
// ),
// ],
// ),
if (hasMultiUserSupport)
CustomScrollView(
controller: _pagingScrollController,
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(
context),
),
switch (state.status) {
LoadingStatus.loaded =>
DocumentPermissionsWidget(
document: state.document!,
),
LoadingStatus.error => _buildErrorState(),
_ => _buildLoadingState(),
}
],
),
],
),
),
@@ -383,8 +379,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return BottomAppBar(
child: Builder(
builder: (context) {
return switch (state) {
DocumentDetailsLoaded(document: var document) => Row(
return switch (state.status) {
LoadingStatus.loaded => Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ConnectivityAwareActionWrapper(
@@ -398,7 +394,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed: () => _onDelete(document),
onPressed: () => _onDelete(state.document!),
).paddedSymmetrically(horizontal: 4),
),
ConnectivityAwareActionWrapper(
@@ -408,7 +404,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
enabled: false,
),
child: DocumentDownloadButton(
document: document,
document: state.document,
),
),
ConnectivityAwareActionWrapper(
@@ -422,7 +418,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
onPressed: _onOpenFileInSystemViewer,
).paddedOnly(right: 4.0),
),
DocumentShareButton(document: document),
DocumentShareButton(document: state.document),
IconButton(
tooltip: S.of(context)!.print,
onPressed: () => context

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/loading_status.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
@@ -50,16 +51,13 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) =>
previous is DocumentDetailsLoaded &&
current is DocumentDetailsLoaded &&
previous.document.archiveSerialNumber !=
current.document.archiveSerialNumber,
previous.status == LoadingStatus.loaded &&
current.status == LoadingStatus.loaded &&
previous.document!.archiveSerialNumber !=
current.document!.archiveSerialNumber,
listener: (context, state) {
_asnEditingController.text = (state as DocumentDetailsLoaded)
.document
.archiveSerialNumber
?.toString() ??
'';
_asnEditingController.text =
state.document!.archiveSerialNumber?.toString() ?? '';
setState(() {
_canUpdate = false;
});

View File

@@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/repository/user_repository.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -69,6 +70,7 @@ class DocumentMetaDataWidget extends StatelessWidget {
context: context,
label: S.of(context)!.originalMIMEType,
).paddedOnly(bottom: itemSpacing),
],
),
);

View File

@@ -27,7 +27,7 @@ class DocumentOverviewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = context.watch<LocalUserAccount>().paperlessUser;
final availableLabels = context.watch<LabelRepository>().state;
final labelRepository = context.watch<LabelRepository>();
return SliverList.list(
children: [
@@ -51,7 +51,7 @@ class DocumentOverviewWidget extends StatelessWidget {
label: S.of(context)!.documentType,
content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge,
label: availableLabels.documentTypes[document.documentType],
label: labelRepository.documentTypes[document.documentType],
),
).paddedOnly(bottom: itemSpacing),
if (document.correspondent != null && user.canViewCorrespondents)
@@ -59,14 +59,14 @@ class DocumentOverviewWidget extends StatelessWidget {
label: S.of(context)!.correspondent,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
label: availableLabels.correspondents[document.correspondent],
label: labelRepository.correspondents[document.correspondent],
),
).paddedOnly(bottom: itemSpacing),
if (document.storagePath != null && user.canViewStoragePaths)
DetailsItem(
label: S.of(context)!.storagePath,
content: LabelText<StoragePath>(
label: availableLabels.storagePaths[document.storagePath],
label: labelRepository.storagePaths[document.storagePath],
),
).paddedOnly(bottom: itemSpacing),
if (document.tags.isNotEmpty && user.canViewTags)
@@ -77,7 +77,7 @@ class DocumentOverviewWidget extends StatelessWidget {
child: TagsWidget(
isClickable: false,
tags:
document.tags.map((e) => availableLabels.tags[e]!).toList(),
document.tags.map((e) => labelRepository.tags[e]!).toList(),
),
),
).paddedOnly(bottom: itemSpacing),

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/user_repository.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
class DocumentPermissionsWidget extends StatefulWidget {
final DocumentModel document;
@@ -13,8 +16,20 @@ class DocumentPermissionsWidget extends StatefulWidget {
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {
@override
Widget build(BuildContext context) {
return const SliverToBoxAdapter(
child: Placeholder(),
return BlocBuilder<UserRepository, UserRepositoryState>(
builder: (context, state) {
final owner = state.users[widget.document.owner];
return SliverList.list(
children: [
if (owner != null)
DetailsItem.text(
owner.username,
label: 'Owner',
context: context,
),
],
);
},
);
}
}

View File

@@ -185,6 +185,8 @@ class _DocumentEditPageState extends State<DocumentEditPage>
Padding _buildEditForm(BuildContext context, DocumentEditState state,
FieldSuggestions? filteredSuggestions, UserModel currentUser) {
final labelRepository = context.watch<LabelRepository>();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TabBarView(
@@ -211,8 +213,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
).push<Correspondent>(context),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options:
context.watch<LabelRepository>().state.correspondents,
options: labelRepository.correspondents,
initialValue: state.document.correspondent != null
? SetIdQueryParameter(
id: state.document.correspondent!)
@@ -243,8 +244,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
? SetIdQueryParameter(
id: state.document.documentType!)
: const UnsetIdQueryParameter(),
options:
context.watch<LabelRepository>().state.documentTypes,
options: labelRepository.documentTypes,
name: _DocumentEditPageState.fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
@@ -266,8 +266,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
canCreateNewLabel: currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options:
context.watch<LabelRepository>().state.storagePaths,
options: labelRepository.storagePaths,
initialValue: state.document.storagePath != null
? SetIdQueryParameter(id: state.document.storagePath!)
: const UnsetIdQueryParameter(),
@@ -280,7 +279,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
// Tag form field
if (currentUser.canViewTags)
TagsFormField(
options: context.watch<LabelRepository>().state.tags,
options: labelRepository.tags,
name: fkTags,
allowOnlySelection: true,
allowCreation: true,

View File

@@ -3,20 +3,24 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/loading_status.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:rxdart/rxdart.dart';
part 'document_scanner_cubit.freezed.dart';
part 'document_scanner_state.dart';
class DocumentScannerCubit extends Cubit<DocumentScannerState> {
final LocalNotificationService _notificationService;
DocumentScannerCubit(this._notificationService)
: super(const InitialDocumentScannerState());
: super(const DocumentScannerState());
Future<void> initialize() async {
logger.fd(
@@ -24,7 +28,7 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
className: runtimeType.toString(),
methodName: "initialize",
);
emit(const RestoringDocumentScannerState());
emit(const DocumentScannerState(status: LoadingStatus.loading));
final tempDir = FileService.instance.temporaryScansDirectory;
final allFiles = tempDir.list().whereType<File>();
final scans =
@@ -36,13 +40,14 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
);
emit(
scans.isEmpty
? const InitialDocumentScannerState()
: LoadedDocumentScannerState(scans: scans),
? const DocumentScannerState()
: DocumentScannerState(scans: scans, status: LoadingStatus.loaded),
);
}
void addScan(File file) async {
emit(LoadedDocumentScannerState(
emit(DocumentScannerState(
status: LoadingStatus.loaded,
scans: [...state.scans, file],
));
}
@@ -60,21 +65,22 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
final scans = state.scans..remove(file);
emit(
scans.isEmpty
? const InitialDocumentScannerState()
: LoadedDocumentScannerState(scans: scans),
? const DocumentScannerState()
: DocumentScannerState(
status: LoadingStatus.loaded,
scans: scans,
),
);
}
Future<void> reset() async {
try {
Future.wait([
for (final file in state.scans) file.delete(),
]);
Future.wait([for (final file in state.scans) file.delete()]);
imageCache.clear();
} catch (_) {
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
addError(TransientPaperlessApiError(code: ErrorCode.scanRemoveFailed));
} finally {
emit(const InitialDocumentScannerState());
emit(const DocumentScannerState());
}
}
@@ -83,6 +89,7 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
String fileName,
String locale,
) async {
try {
var file = await FileService.instance.saveToFile(bytes, fileName);
_notificationService.notifyFileSaved(
filename: fileName,
@@ -90,5 +97,8 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
finished: true,
locale: locale,
);
} on Exception catch (e) {
addError(TransientMessageError(message: e.toString()));
}
}
}

View File

@@ -1,30 +1,9 @@
part of 'document_scanner_cubit.dart';
sealed class DocumentScannerState {
final List<File> scans;
const DocumentScannerState({
this.scans = const [],
});
}
class InitialDocumentScannerState extends DocumentScannerState {
const InitialDocumentScannerState();
}
class RestoringDocumentScannerState extends DocumentScannerState {
const RestoringDocumentScannerState({super.scans});
}
class LoadedDocumentScannerState extends DocumentScannerState {
const LoadedDocumentScannerState({super.scans});
}
class ErrorDocumentScannerState extends DocumentScannerState {
final String message;
const ErrorDocumentScannerState({
required this.message,
super.scans,
});
@freezed
class DocumentScannerState with _$DocumentScannerState {
const factory DocumentScannerState({
@Default(LoadingStatus.initial) LoadingStatus status,
@Default([]) List<File> scans,
}) = _DocumentScannerState;
}

View File

@@ -10,6 +10,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/loading_status.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/global/constants.dart';
@@ -78,13 +79,11 @@ class _ScannerPageState extends State<ScannerPage>
],
body: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
builder: (context, state) {
return switch (state) {
InitialDocumentScannerState() => _buildEmptyState(),
RestoringDocumentScannerState() => Center(
child: Text("Restoring..."),
),
LoadedDocumentScannerState() => _buildImageGrid(state.scans),
ErrorDocumentScannerState() => Placeholder(),
return switch (state.status) {
LoadingStatus.initial => _buildEmptyState(),
LoadingStatus.loading => Center(child: Text("Restoring...")),
LoadingStatus.loaded => _buildImageGrid(state.scans),
LoadingStatus.error => Placeholder(),
};
},
),

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
@@ -33,6 +34,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
DateTime? createdAt,
int? asn,
}) async {
try {
final taskId = await _documentApi.create(
bytes,
filename: filename,
@@ -52,5 +54,11 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
_tasksNotifier.listenToTaskChanges(taskId);
}
return taskId;
} on PaperlessApiException catch (error) {
addError(TransientPaperlessApiError(
code: error.code,
details: error.details,
));
}
}
}

View File

@@ -70,7 +70,7 @@ class _DocumentUploadPreparationPageState
@override
Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state;
final labelRepository = context.watch<LabelRepository>();
return BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, state) {
return Scaffold(
@@ -242,7 +242,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: labels.correspondents,
options: labelRepository.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: context
@@ -265,7 +265,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey,
options: labels.documentTypes,
options: labelRepository.documentTypes,
prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
@@ -283,7 +283,7 @@ class _DocumentUploadPreparationPageState
allowCreation: true,
allowExclude: false,
allowOnlySelection: true,
options: labels.tags,
options: labelRepository.tags,
),
Text(
"* " + S.of(context)!.uploadInferValuesHint,
@@ -353,8 +353,6 @@ class _DocumentUploadPreparationPageState
S.of(context)!.documentSuccessfullyUploadedProcessing,
);
context.pop(DocumentUploadResult(true, taskId));
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessFormValidationException catch (exception) {
setState(() => _errors = exception.validationMessages);
} catch (error, stackTrace) {

View File

@@ -1,8 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
@@ -20,7 +18,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
@override
final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override
final ConnectivityStatusService connectivityStatusService;
@@ -32,7 +29,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
DocumentsCubit(
this.api,
this.notifier,
this._labelRepository,
this._userState,
this.connectivityStatusService,
) : super(DocumentsState(
@@ -58,17 +54,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
);
},
);
_labelRepository.addListener(
this,
onChanged: (labels) => emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
),
),
);
}
Future<void> bulkDelete(List<DocumentModel> documents) async {
@@ -111,7 +96,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
@override
Future<void> close() {
notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close();
}

View File

@@ -2,10 +2,6 @@ part of 'documents_cubit.dart';
class DocumentsState extends DocumentPagingState {
final List<DocumentModel> selection;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
final ViewType viewType;
@@ -16,10 +12,6 @@ class DocumentsState extends DocumentPagingState {
super.filter = const DocumentFilter(),
super.hasLoaded = false,
super.isLoading = false,
this.correspondents = const {},
this.documentTypes = const {},
this.tags = const {},
this.storagePaths = const {},
});
List<int> get selectedIds => selection.map((e) => e.id).toList();
@@ -31,10 +23,6 @@ class DocumentsState extends DocumentPagingState {
DocumentFilter? filter,
List<DocumentModel>? selection,
ViewType? viewType,
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, Tag>? tags,
Map<int, StoragePath>? storagePaths,
}) {
return DocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded,
@@ -43,10 +31,6 @@ class DocumentsState extends DocumentPagingState {
filter: filter ?? this.filter,
selection: selection ?? this.selection,
viewType: viewType ?? this.viewType,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
tags: tags ?? this.tags,
storagePaths: storagePaths ?? this.storagePaths,
);
}
@@ -54,10 +38,6 @@ class DocumentsState extends DocumentPagingState {
List<Object?> get props => [
selection,
viewType,
correspondents,
documentTypes,
tags,
storagePaths,
super.filter,
super.hasLoaded,
super.isLoading,

View File

@@ -489,10 +489,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
initialFilter: context.read<DocumentsCubit>().state.filter,
scrollController: controller,
draggableSheetController: draggableSheetController,
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
);
},
),

View File

@@ -18,6 +18,7 @@ class DateAndDocumentTypeLabelWidget extends StatelessWidget {
Widget build(BuildContext context) {
final subtitleStyle =
Theme.of(context).textTheme.labelMedium?.apply(color: Colors.grey);
final labelRepository = context.watch<LabelRepository>();
return RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -37,11 +38,8 @@ class DateAndDocumentTypeLabelWidget extends StatelessWidget {
? () => onDocumentTypeSelected!(document.documentType)
: null,
child: Text(
context
.watch<LabelRepository>()
.state
.documentTypes[document.documentType]!
.name,
labelRepository
.documentTypes[document.documentType]!.name,
style: subtitleStyle,
),
),

View File

@@ -1,21 +1,18 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:provider/provider.dart';
@@ -58,7 +55,7 @@ class DocumentDetailedItem extends DocumentItem {
final maxHeight = highlights != null
? min(600.0, availableHeight)
: min(500.0, availableHeight);
final labels = context.watch<LabelRepository>().state;
final labelRepository = context.watch<LabelRepository>();
return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
child: InkWell(
@@ -93,8 +90,9 @@ class DocumentDetailedItem extends DocumentItem {
Align(
alignment: Alignment.bottomLeft,
child: TagsWidget(
tags:
document.tags.map((e) => labels.tags[e]!).toList(),
tags: document.tags
.map((e) => labelRepository.tags[e]!)
.toList(),
onTagSelected: onTagSelected,
).padded(),
),
@@ -107,7 +105,8 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondent: labels.correspondents[document.correspondent],
correspondent:
labelRepository.correspondents[document.correspondent],
).paddedLTRB(8, 8, 8, 0),
Text(
document.title.isEmpty ? '(-)' : document.title,

View File

@@ -30,6 +30,7 @@ class DocumentGridItem extends DocumentItem {
@override
Widget build(BuildContext context) {
var currentUser = context.watch<LocalUserAccount>().paperlessUser;
final labelRepository = context.watch<LabelRepository>();
return Stack(
children: [
Card(
@@ -75,10 +76,7 @@ class DocumentGridItem extends DocumentItem {
if (currentUser.canViewTags)
TagsWidget.sliver(
tags: document.tags
.map((e) => context
.watch<LabelRepository>()
.state
.tags[e]!)
.map((e) => labelRepository.tags[e]!)
.toList(),
onTagSelected: onTagSelected,
),
@@ -102,17 +100,13 @@ class DocumentGridItem extends DocumentItem {
children: [
if (currentUser.canViewCorrespondents)
CorrespondentWidget(
correspondent: context
.watch<LabelRepository>()
.state
correspondent: labelRepository
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
if (currentUser.canViewDocumentTypes)
DocumentTypeWidget(
documentType: context
.watch<LabelRepository>()
.state
documentType: labelRepository
.documentTypes[document.documentType],
onSelected: onDocumentTypeSelected,
),

View File

@@ -1,8 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/src/models/document_model.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
@@ -32,7 +29,7 @@ class DocumentListItem extends DocumentItem {
@override
Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state;
final labelRepository = context.watch<LabelRepository>();
return ListTile(
tileColor: backgroundColor,
@@ -51,10 +48,8 @@ class DocumentListItem extends DocumentItem {
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
correspondent:
labelRepository.correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
),
@@ -70,8 +65,8 @@ class DocumentListItem extends DocumentItem {
child: TagsWidget(
isClickable: isLabelClickable,
tags: document.tags
.where((e) => labels.tags.containsKey(e))
.map((e) => labels.tags[e]!)
.where((e) => labelRepository.tags.containsKey(e))
.map((e) => labelRepository.tags[e]!)
.toList(),
onTagSelected: (id) => onTagSelected?.call(id),
),

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
@@ -47,10 +48,6 @@ class DocumentFilterForm extends StatefulWidget {
final DocumentFilter initialFilter;
final ScrollController? scrollController;
final EdgeInsets padding;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
const DocumentFilterForm({
super.key,
@@ -59,10 +56,6 @@ class DocumentFilterForm extends StatefulWidget {
required this.initialFilter,
this.scrollController,
this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
});
@override
@@ -80,13 +73,14 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
@override
Widget build(BuildContext context) {
final labelRepository = context.watch<LabelRepository>();
return FormBuilder(
key: widget.formKey,
child: CustomScrollView(
controller: widget.scrollController,
slivers: [
if (widget.header != null) widget.header!,
..._buildFormFieldList(),
..._buildFormFieldList(labelRepository),
const SliverToBoxAdapter(
child: SizedBox(
height: 32,
@@ -97,7 +91,7 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
);
}
List<Widget> _buildFormFieldList() {
List<Widget> _buildFormFieldList(LabelRepository labelRepository) {
return [
_buildQueryFormField(),
Align(
@@ -123,10 +117,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
_checkQueryConstraints();
},
),
_buildCorrespondentFormField(),
_buildDocumentTypeFormField(),
_buildStoragePathFormField(),
_buildTagsFormField(),
_buildCorrespondentFormField(labelRepository.correspondents),
_buildDocumentTypeFormField(labelRepository.documentTypes),
_buildStoragePathFormField(labelRepository.storagePaths),
_buildTagsFormField(labelRepository.tags),
]
.map((w) => SliverPadding(
padding: widget.padding,
@@ -151,10 +145,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
}
}
Widget _buildDocumentTypeFormField() {
Widget _buildDocumentTypeFormField(Map<int, DocumentType> documentTypes) {
return LabelFormField<DocumentType>(
name: DocumentFilterForm.fkDocumentType,
options: widget.documentTypes,
options: documentTypes,
labelText: S.of(context)!.documentType,
initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined),
@@ -166,10 +160,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
);
}
Widget _buildCorrespondentFormField() {
Widget _buildCorrespondentFormField(Map<int, Correspondent> correspondents) {
return LabelFormField<Correspondent>(
name: DocumentFilterForm.fkCorrespondent,
options: widget.correspondents,
options: correspondents,
labelText: S.of(context)!.correspondent,
initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline),
@@ -181,10 +175,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
);
}
Widget _buildStoragePathFormField() {
Widget _buildStoragePathFormField(Map<int, StoragePath> storagePaths) {
return LabelFormField<StoragePath>(
name: DocumentFilterForm.fkStoragePath,
options: widget.storagePaths,
options: storagePaths,
labelText: S.of(context)!.storagePath,
initialValue: widget.initialFilter.storagePath,
prefixIcon: const Icon(Icons.folder_outlined),
@@ -202,11 +196,11 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
);
}
Widget _buildTagsFormField() {
Widget _buildTagsFormField(Map<int, Tag> tags) {
return TagsFormField(
name: DocumentModel.tagsKey,
initialValue: widget.initialFilter.tags,
options: widget.tags,
options: tags,
allowExclude: false,
allowOnlySelection: false,
allowCreation: false,

View File

@@ -13,20 +13,12 @@ class DocumentFilterPanel extends StatefulWidget {
final DocumentFilter initialFilter;
final ScrollController scrollController;
final DraggableScrollableController draggableSheetController;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
const DocumentFilterPanel({
Key? key,
required this.initialFilter,
required this.scrollController,
required this.draggableSheetController,
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
}) : super(key: key);
@override
@@ -104,10 +96,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
scrollController: widget.scrollController,
initialFilter: widget.initialFilter,
header: _buildPanelHeader(),
correspondents: widget.correspondents,
documentTypes: widget.documentTypes,
storagePaths: widget.storagePaths,
tags: widget.tags,
),
),
);

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -8,10 +10,6 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget {
final SortOrder initialSortOrder;
final SortField? initialSortField;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
final Future Function(SortField? field, SortOrder order) onSubmit;
@@ -20,10 +18,6 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
required this.initialSortOrder,
required this.initialSortField,
required this.onSubmit,
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
});
@override
@@ -45,6 +39,7 @@ class _SortFieldSelectionBottomSheetState
@override
Widget build(BuildContext context) {
final labelRepository = context.watch<LabelRepository>();
return ClipRRect(
child: SingleChildScrollView(
child: Column(
@@ -75,7 +70,7 @@ class _SortFieldSelectionBottomSheetState
_buildSortOption(SortField.archiveSerialNumber),
_buildSortOption(
SortField.correspondentName,
enabled: widget.correspondents.values.fold<bool>(
enabled: labelRepository.correspondents.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0),
@@ -83,7 +78,7 @@ class _SortFieldSelectionBottomSheetState
_buildSortOption(SortField.title),
_buildSortOption(
SortField.documentType,
enabled: widget.documentTypes.values.fold<bool>(
enabled: labelRepository.documentTypes.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0),

View File

@@ -69,10 +69,6 @@ class SortDocumentsButton extends StatelessWidget {
),
);
},
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
),
),
),

View File

@@ -1,33 +0,0 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'edit_label_state.dart';
part 'edit_label_cubit.freezed.dart';
class EditLabelCubit extends Cubit<EditLabelState>
with LabelCubitMixin<EditLabelState> {
@override
final LabelRepository labelRepository;
EditLabelCubit(this.labelRepository) : super(const EditLabelState()) {
labelRepository.addListener(
this,
onChanged: (labels) => state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
),
);
}
@override
Future<void> close() {
labelRepository.removeListener(this);
return super.close();
}
}

View File

@@ -1,11 +0,0 @@
part of 'edit_label_cubit.dart';
@freezed
class EditLabelState with _$EditLabelState {
const factory EditLabelState({
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,
@Default({}) Map<int, StoragePath> storagePaths,
}) = _EditLabelState;
}

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddLabelPage<T extends Label> extends StatelessWidget {
@@ -25,7 +25,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read<LabelRepository>(),
),
child: AddLabelFormWidget(

View File

@@ -9,8 +9,8 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -35,7 +35,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read<LabelRepository>(),
),
child: EditLabelForm(

View File

@@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddCorrespondentPage extends StatelessWidget {
@@ -12,7 +12,7 @@ class AddCorrespondentPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: AddLabelPage<Correspondent>(
@@ -20,7 +20,7 @@ class AddCorrespondentPage extends StatelessWidget {
fromJsonT: Correspondent.fromJson,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addCorrespondent(label),
context.read<LabelCubit>().addCorrespondent(label),
),
);
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddDocumentTypePage extends StatelessWidget {
@@ -15,7 +15,7 @@ class AddDocumentTypePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: AddLabelPage<DocumentType>(
@@ -23,7 +23,7 @@ class AddDocumentTypePage extends StatelessWidget {
fromJsonT: DocumentType.fromJson,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addDocumentType(label),
context.read<LabelCubit>().addDocumentType(label),
),
);
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -13,7 +13,7 @@ class AddStoragePathPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: AddLabelPage<StoragePath>(
@@ -21,7 +21,7 @@ class AddStoragePathPage extends StatelessWidget {
fromJsonT: StoragePath.fromJson,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label),
context.read<LabelCubit>().addStoragePath(label),
additionalFields: const [
StoragePathAutofillFormBuilderField(name: StoragePath.pathKey),
SizedBox(height: 120.0),

View File

@@ -5,8 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddTagPage extends StatelessWidget {
@@ -16,15 +16,14 @@ class AddTagPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context)!.addTag),
fromJsonT: Tag.fromJson,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addTag(label),
onSubmit: (context, label) => context.read<LabelCubit>().addTag(label),
additionalFields: [
FormBuilderColorPickerField(
name: Tag.colorKey,

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class EditCorrespondentPage extends StatelessWidget {
final Correspondent correspondent;
@@ -13,7 +13,7 @@ class EditCorrespondentPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
lazy: false,
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: Builder(builder: (context) {
@@ -21,9 +21,9 @@ class EditCorrespondentPage extends StatelessWidget {
label: correspondent,
fromJsonT: Correspondent.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceCorrespondent(label),
context.read<LabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
context.read<LabelCubit>().removeCorrespondent(label),
canDelete: context
.watch<LocalUserAccount>()
.paperlessUser

View File

@@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class EditDocumentTypePage extends StatelessWidget {
final DocumentType documentType;
@@ -12,16 +12,16 @@ class EditDocumentTypePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: EditLabelPage<DocumentType>(
label: documentType,
fromJsonT: DocumentType.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceDocumentType(label),
context.read<LabelCubit>().replaceDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
context.read<LabelCubit>().removeDocumentType(label),
canDelete: context
.watch<LocalUserAccount>()
.paperlessUser

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
class EditStoragePathPage extends StatelessWidget {
@@ -13,16 +13,16 @@ class EditStoragePathPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: EditLabelPage<StoragePath>(
label: storagePath,
fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceStoragePath(label),
context.read<LabelCubit>().replaceStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
context.read<LabelCubit>().removeStoragePath(label),
canDelete: context
.watch<LocalUserAccount>()
.paperlessUser

View File

@@ -4,8 +4,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class EditTagPage extends StatelessWidget {
@@ -16,16 +16,16 @@ class EditTagPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
create: (context) => LabelCubit(
context.read(),
),
child: EditLabelPage<Tag>(
label: tag,
fromJsonT: Tag.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceTag(label),
context.read<LabelCubit>().replaceTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
context.read<LabelCubit>().removeTag(label),
canDelete:
context.watch<LocalUserAccount>().paperlessUser.canDeleteTags,
additionalFields: [

View File

@@ -53,8 +53,8 @@ class HomeShellWidget extends StatelessWidget {
builder: (context, box, _) {
if (currentUserId == null) {
//This only happens during logout...
//TODO: Find way so this does not occur anymore
return SizedBox.shrink();
//FIXME: Find way so this does not occur anymore
return const SizedBox.shrink();
}
final currentLocalUser = box.get(currentUserId)!;
return MultiProvider(
@@ -107,36 +107,31 @@ class HomeShellWidget extends StatelessWidget {
),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => PaperlessUserApiV3Impl(
create: (context) => paperlessProviderFactory.createUserApi(
context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
),
),
],
builder: (context, _) {
return MultiProvider(
providers: [
Provider(
ChangeNotifierProvider(
create: (context) {
final repo = LabelRepository(context.read());
if (currentLocalUser
.paperlessUser.canViewCorrespondents) {
repo.findAllCorrespondents();
}
if (currentLocalUser
.paperlessUser.canViewDocumentTypes) {
repo.findAllDocumentTypes();
}
if (currentLocalUser.paperlessUser.canViewTags) {
repo.findAllTags();
}
if (currentLocalUser
.paperlessUser.canViewStoragePaths) {
repo.findAllStoragePaths();
}
return repo;
return LabelRepository(context.read())
..initialize(
loadCorrespondents: currentLocalUser
.paperlessUser.canViewCorrespondents,
loadDocumentTypes: currentLocalUser
.paperlessUser.canViewDocumentTypes,
loadStoragePaths: currentLocalUser
.paperlessUser.canViewStoragePaths,
loadTags:
currentLocalUser.paperlessUser.canViewTags,
);
},
),
Provider(
ChangeNotifierProvider(
create: (context) {
final repo = SavedViewRepository(context.read());
if (currentLocalUser.paperlessUser.canViewSavedViews) {
@@ -145,6 +140,12 @@ class HomeShellWidget extends StatelessWidget {
return repo;
},
),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => UserRepository(
context.read(),
)..initialize(),
),
],
builder: (context, _) {
return MultiProvider(
@@ -152,7 +153,6 @@ class HomeShellWidget extends StatelessWidget {
Provider(
lazy: false,
create: (context) => DocumentsCubit(
context.read(),
context.read(),
context.read(),
Hive.box<LocalUserAppState>(
@@ -196,12 +196,6 @@ class HomeShellWidget extends StatelessWidget {
context.read(),
),
),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => UserRepository(
context.read(),
)..initialize(),
),
],
child: child,
);

View File

@@ -6,7 +6,6 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
@@ -37,7 +36,7 @@ class InboxCubit extends HydratedCubit<InboxState>
this._labelRepository,
this.notifier,
this.connectivityStatusService,
) : super(InboxState(labels: _labelRepository.state)) {
) : super(const InboxState()) {
notifier.addListener(
this,
onDeleted: remove,
@@ -62,12 +61,6 @@ class InboxCubit extends HydratedCubit<InboxState>
}
},
);
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(state.copyWith(labels: labels));
},
);
}
@override
@@ -256,7 +249,6 @@ class InboxCubit extends HydratedCubit<InboxState>
@override
Future<void> close() {
_labelRepository.removeListener(this);
return super.close();
}

View File

@@ -4,8 +4,6 @@ part of 'inbox_cubit.dart';
class InboxState extends DocumentPagingState {
final Iterable<int> inboxTags;
final LabelRepositoryState labels;
final int itemsInInboxCount;
@JsonKey()
@@ -19,7 +17,6 @@ class InboxState extends DocumentPagingState {
this.inboxTags = const [],
this.isHintAcknowledged = false,
this.itemsInInboxCount = 0,
this.labels = const LabelRepositoryState(),
});
@override
@@ -32,7 +29,6 @@ class InboxState extends DocumentPagingState {
documents,
isHintAcknowledged,
itemsInInboxCount,
labels,
];
InboxState copyWith({
@@ -42,7 +38,6 @@ class InboxState extends DocumentPagingState {
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
bool? isHintAcknowledged,
LabelRepositoryState? labels,
Map<int, FieldSuggestions>? suggestions,
int? itemsInInboxCount,
}) {
@@ -52,7 +47,6 @@ class InboxState extends DocumentPagingState {
value: value ?? super.value,
inboxTags: inboxTags ?? this.inboxTags,
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
labels: labels ?? this.labels,
filter: filter ?? super.filter,
itemsInInboxCount: itemsInInboxCount ?? this.itemsInInboxCount,
);

View File

@@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/util/lambda_utils.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
@@ -148,6 +150,7 @@ class _InboxItemState extends State<InboxItem> {
@override
Widget build(BuildContext context) {
final labelRepository = context.read<LabelRepository>();
return BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return GestureDetector(
@@ -193,7 +196,7 @@ class _InboxItemState extends State<InboxItem> {
?.fontSize,
),
LabelText<Correspondent>(
label: state.labels.correspondents[
label: labelRepository.correspondents[
widget.document.correspondent],
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
@@ -208,7 +211,7 @@ class _InboxItemState extends State<InboxItem> {
?.fontSize,
),
LabelText<DocumentType>(
label: state.labels.documentTypes[
label: labelRepository.documentTypes[
widget.document.documentType],
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
@@ -217,8 +220,8 @@ class _InboxItemState extends State<InboxItem> {
const Spacer(),
TagsWidget(
tags: widget.document.tags
.map((e) => state.labels.tags[e])
.whereNot((e) => e == null)
.map((e) => labelRepository.tags[e])
.where(isNotNull)
.toList()
.cast<Tag>(),
isClickable: false,

View File

@@ -2,41 +2,134 @@ import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
part 'label_cubit.freezed.dart';
part 'label_state.dart';
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
@override
class LabelCubit extends Cubit<LabelState> {
final LabelRepository labelRepository;
LabelCubit(this.labelRepository) : super(const LabelState()) {
labelRepository.addListener(
this,
onChanged: (labels) {
() {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
correspondents: labelRepository.correspondents,
documentTypes: labelRepository.documentTypes,
storagePaths: labelRepository.storagePaths,
tags: labelRepository.tags,
));
},
);
}
Future<void> reload() {
return Future.wait([
labelRepository.findAllCorrespondents(),
labelRepository.findAllDocumentTypes(),
labelRepository.findAllTags(),
labelRepository.findAllStoragePaths(),
]);
Future<void> reload({
required bool loadCorrespondents,
required bool loadDocumentTypes,
required bool loadStoragePaths,
required bool loadTags,
}) {
return labelRepository.initialize(
loadCorrespondents: loadCorrespondents,
loadDocumentTypes: loadDocumentTypes,
loadStoragePaths: loadStoragePaths,
loadTags: loadTags,
);
}
Future<Correspondent> addCorrespondent(Correspondent item) async {
assert(item.id == null);
final addedItem = await labelRepository.createCorrespondent(item);
return addedItem;
}
Future<void> reloadCorrespondents() {
return labelRepository.findAllCorrespondents();
}
Future<Correspondent> replaceCorrespondent(Correspondent item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateCorrespondent(item);
return updatedItem;
}
Future<void> removeCorrespondent(Correspondent item) async {
assert(item.id != null);
if (labelRepository.correspondents.containsKey(item.id)) {
await labelRepository.deleteCorrespondent(item);
}
}
Future<DocumentType> addDocumentType(DocumentType item) async {
assert(item.id == null);
final addedItem = await labelRepository.createDocumentType(item);
return addedItem;
}
Future<void> reloadDocumentTypes() {
return labelRepository.findAllDocumentTypes();
}
Future<DocumentType> replaceDocumentType(DocumentType item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateDocumentType(item);
return updatedItem;
}
Future<void> removeDocumentType(DocumentType item) async {
assert(item.id != null);
if (labelRepository.documentTypes.containsKey(item.id)) {
await labelRepository.deleteDocumentType(item);
}
}
Future<StoragePath> addStoragePath(StoragePath item) async {
assert(item.id == null);
final addedItem = await labelRepository.createStoragePath(item);
return addedItem;
}
Future<void> reloadStoragePaths() {
return labelRepository.findAllStoragePaths();
}
Future<StoragePath> replaceStoragePath(StoragePath item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateStoragePath(item);
return updatedItem;
}
Future<void> removeStoragePath(StoragePath item) async {
assert(item.id != null);
if (labelRepository.storagePaths.containsKey(item.id)) {
await labelRepository.deleteStoragePath(item);
}
}
Future<Tag> addTag(Tag item) async {
assert(item.id == null);
final addedItem = await labelRepository.createTag(item);
return addedItem;
}
Future<void> reloadTags() {
return labelRepository.findAllTags();
}
Future<Tag> replaceTag(Tag item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateTag(item);
return updatedItem;
}
Future<void> removeTag(Tag item) async {
assert(item.id != null);
if (labelRepository.tags.containsKey(item.id)) {
await labelRepository.deleteTag(item);
}
}
@override
Future<void> close() {
labelRepository.removeListener(this);
return super.close();
}
}

View File

@@ -1,102 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
///
/// Mixin which adds functionality to manage labels to [Bloc]s.
///
mixin LabelCubitMixin<T> on BlocBase<T> {
LabelRepository get labelRepository;
Future<Correspondent> addCorrespondent(Correspondent item) async {
assert(item.id == null);
final addedItem = await labelRepository.createCorrespondent(item);
return addedItem;
}
Future<void> reloadCorrespondents() {
return labelRepository.findAllCorrespondents();
}
Future<Correspondent> replaceCorrespondent(Correspondent item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateCorrespondent(item);
return updatedItem;
}
Future<void> removeCorrespondent(Correspondent item) async {
assert(item.id != null);
if (labelRepository.state.correspondents.containsKey(item.id)) {
await labelRepository.deleteCorrespondent(item);
}
}
Future<DocumentType> addDocumentType(DocumentType item) async {
assert(item.id == null);
final addedItem = await labelRepository.createDocumentType(item);
return addedItem;
}
Future<void> reloadDocumentTypes() {
return labelRepository.findAllDocumentTypes();
}
Future<DocumentType> replaceDocumentType(DocumentType item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateDocumentType(item);
return updatedItem;
}
Future<void> removeDocumentType(DocumentType item) async {
assert(item.id != null);
if (labelRepository.state.documentTypes.containsKey(item.id)) {
await labelRepository.deleteDocumentType(item);
}
}
Future<StoragePath> addStoragePath(StoragePath item) async {
assert(item.id == null);
final addedItem = await labelRepository.createStoragePath(item);
return addedItem;
}
Future<void> reloadStoragePaths() {
return labelRepository.findAllStoragePaths();
}
Future<StoragePath> replaceStoragePath(StoragePath item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateStoragePath(item);
return updatedItem;
}
Future<void> removeStoragePath(StoragePath item) async {
assert(item.id != null);
if (labelRepository.state.storagePaths.containsKey(item.id)) {
await labelRepository.deleteStoragePath(item);
}
}
Future<Tag> addTag(Tag item) async {
assert(item.id == null);
final addedItem = await labelRepository.createTag(item);
return addedItem;
}
Future<void> reloadTags() {
return labelRepository.findAllTags();
}
Future<Tag> replaceTag(Tag item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateTag(item);
return updatedItem;
}
Future<void> removeTag(Tag item) async {
assert(item.id != null);
if (labelRepository.state.tags.containsKey(item.id)) {
await labelRepository.deleteTag(item);
}
}
}

View File

@@ -19,28 +19,13 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
final ConnectivityStatusService connectivityStatusService;
@override
final DocumentChangedNotifier notifier;
final LabelRepository _labelRepository;
LinkedDocumentsCubit(
DocumentFilter filter,
this.api,
this.notifier,
this._labelRepository,
this.connectivityStatusService,
) : super(LinkedDocumentsState(filter: filter)) {
updateFilter(filter: filter);
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
));
},
);
notifier.addListener(
this,
onUpdated: replace,

View File

@@ -5,21 +5,12 @@ class LinkedDocumentsState extends DocumentPagingState {
@JsonKey()
final ViewType viewType;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, StoragePath> storagePaths;
final Map<int, Tag> tags;
const LinkedDocumentsState({
this.viewType = ViewType.list,
super.filter = const DocumentFilter(),
super.isLoading,
super.hasLoaded,
super.value,
this.correspondents = const {},
this.documentTypes = const {},
this.storagePaths = const {},
this.tags = const {},
});
LinkedDocumentsState copyWith({
@@ -39,10 +30,6 @@ class LinkedDocumentsState extends DocumentPagingState {
hasLoaded: hasLoaded ?? this.hasLoaded,
value: value ?? this.value,
viewType: viewType ?? this.viewType,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
storagePaths: storagePaths ?? this.storagePaths,
tags: tags ?? this.tags,
);
}
@@ -64,10 +51,6 @@ class LinkedDocumentsState extends DocumentPagingState {
@override
List<Object?> get props => [
viewType,
correspondents,
documentTypes,
tags,
storagePaths,
...super.props,
];

View File

@@ -112,6 +112,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async {
emit(const SwitchingAccountsState());
await FileService.instance.initialize();
final redactedId = redactUserId(localUserId);
logger.fd(
'Trying to switch to user $redactedId...',

View File

@@ -1,9 +1,13 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
@@ -13,10 +17,13 @@ import 'package:paperless_mobile/features/login/model/client_certificate_form_mo
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/login_settings_page.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/generated/assets.gen.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routing/routes/app_logs_route.dart';
class AddAccountPage extends StatefulWidget {
final FutureOr<void> Function(
@@ -58,10 +65,172 @@ class _AddAccountPageState extends State<AddAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
bool _isCheckingConnection = false;
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
bool _isFormSubmitted = false;
final _pageController = PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(widget.titleText),
),
body: FormBuilder(
key: _formKey,
child: AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Assets.logos.paperlessLogoGreenPng.image(
width: 150,
height: 150,
),
Text(
'Paperless Mobile',
style: Theme.of(context).textTheme.displaySmall,
).padded(),
SizedBox(height: 24),
Expanded(
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: _pageController,
allowImplicitScrolling: false,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ServerAddressFormField(
onChanged: (value) {
setState(() {
_reachabilityStatus = ReachabilityStatus.unknown;
});
},
).paddedSymmetrically(
horizontal: 12,
vertical: 12,
),
ClientCertificateFormField(
initialBytes: widget.initialClientCertificate?.bytes,
initialPassphrase:
widget.initialClientCertificate?.passphrase,
).padded(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
//TODO: Move additional headers and client cert to separate page
// IconButton.filledTonal(
// onPressed: () {
// Navigator.of(context).push(
// MaterialPageRoute(builder: (context) {
// return LoginSettingsPage();
// }),
// );
// },
// icon: Icon(Icons.settings),
// ),
SizedBox(width: 8),
FilledButton.icon(
onPressed: () async {
final status = await _updateReachability();
if (status == ReachabilityStatus.reachable) {
Future.delayed(1.seconds, () {
_pageController.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
});
}
},
icon: _isCheckingConnection
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context)
.colorScheme
.onSecondary,
),
)
: _reachabilityStatus ==
ReachabilityStatus.reachable
? Icon(Icons.done)
: Icon(Icons.arrow_forward),
label: Text(S.of(context)!.continueLabel),
),
],
).paddedSymmetrically(
horizontal: 16,
vertical: 8,
),
_buildStatusIndicator().padded(),
],
),
Column(
children: [
UserCredentialsFormField(
formKey: _formKey,
initialUsername: widget.initialUsername,
initialPassword: widget.initialPassword,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
_pageController.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
icon: Icon(Icons.arrow_back),
label: Text(S.of(context)!.edit),
),
FilledButton(
onPressed: () {
_onSubmit();
},
child: Text(S.of(context)!.signIn),
),
],
).padded(),
Text(
S.of(context)!.loginRequiredPermissionsHint,
style: Theme.of(context).textTheme.bodySmall?.apply(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.6),
),
).padded(16),
],
),
],
),
),
Text.rich(
TextSpan(
style: Theme.of(context).textTheme.labelLarge,
children: [
TextSpan(text: S.of(context)!.version(packageInfo.version)),
WidgetSpan(child: SizedBox(width: 24)),
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
text: S.of(context)!.appLogs(''),
recognizer: TapGestureRecognizer()
..onTap = () {
AppLogsRoute().push(context);
},
),
],
),
).padded(),
],
),
),
),
);
return Scaffold(
appBar: AppBar(
title: Text(widget.titleText),
@@ -91,7 +260,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
children: [
ServerAddressFormField(
initialValue: widget.initialServerUrl,
onSubmit: (address) {
onChanged: (address) {
_updateReachability(address);
},
).padded(),
@@ -117,7 +286,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
.withOpacity(0.6),
),
).padded(16),
]
],
],
),
),
@@ -125,7 +294,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
);
}
Future<void> _updateReachability([String? address]) async {
Future<ReachabilityStatus> _updateReachability([String? address]) async {
setState(() {
_isCheckingConnection = true;
});
@@ -150,13 +319,10 @@ class _AddAccountPageState extends State<AddAccountPage> {
_isCheckingConnection = false;
_reachabilityStatus = status;
});
return status;
}
Widget _buildStatusIndicator() {
if (_isCheckingConnection) {
return const ListTile();
}
Widget _buildIconText(
IconData icon,
String text, [
@@ -176,14 +342,6 @@ class _AddAccountPageState extends State<AddAccountPage> {
Color errorColor = Theme.of(context).colorScheme.error;
switch (_reachabilityStatus) {
case ReachabilityStatus.unknown:
return Container();
case ReachabilityStatus.reachable:
return _buildIconText(
Icons.done,
S.of(context)!.connectionSuccessfulylEstablished,
Colors.green,
);
case ReachabilityStatus.notReachable:
return _buildIconText(
Icons.close,
@@ -214,6 +372,8 @@ class _AddAccountPageState extends State<AddAccountPage> {
S.of(context)!.connectionTimedOut,
errorColor,
);
default:
return const ListTile();
}
}

View File

@@ -7,7 +7,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:path/path.dart' as p;
import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget {
@@ -16,10 +17,10 @@ class ClientCertificateFormField extends StatefulWidget {
final String? initialPassphrase;
final Uint8List? initialBytes;
final void Function(ClientCertificateFormModel? cert) onChanged;
final ValueChanged<ClientCertificateFormModel?>? onChanged;
const ClientCertificateFormField({
super.key,
required this.onChanged,
this.onChanged,
this.initialPassphrase,
this.initialBytes,
});
@@ -29,13 +30,15 @@ class ClientCertificateFormField extends StatefulWidget {
_ClientCertificateFormFieldState();
}
class _ClientCertificateFormFieldState
extends State<ClientCertificateFormField> {
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
with AutomaticKeepAliveClientMixin {
File? _selectedFile;
@override
Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<ClientCertificateFormModel?>(
key: const ValueKey('login-client-cert'),
name: ClientCertificateFormField.fkClientCertificate,
onChanged: widget.onChanged,
initialValue: widget.initialBytes != null
? ClientCertificateFormModel(
@@ -43,16 +46,6 @@ class _ClientCertificateFormFieldState
passphrase: widget.initialPassphrase,
)
: null,
validator: (value) {
if (value == null) {
return null;
}
assert(_selectedFile != null);
if (_selectedFile?.path.split(".").last != 'pfx') {
return S.of(context)!.invalidCertificateFormat;
}
return null;
},
builder: (field) {
final theme =
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
@@ -127,7 +120,6 @@ class _ClientCertificateFormFieldState
),
);
},
name: ClientCertificateFormField.fkClientCertificate,
);
}
@@ -140,6 +132,11 @@ class _ClientCertificateFormFieldState
if (result == null || result.files.single.path == null) {
return;
}
final path = result.files.single.path!;
if (p.extension(path) != '.pfx') {
showSnackBar(context, S.of(context)!.invalidCertificateFormat);
return;
}
File file = File(result.files.single.path!);
setState(() {
_selectedFile = file;
@@ -171,4 +168,7 @@ class _ClientCertificateFormFieldState
);
}
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class LoginSettingsPage extends StatelessWidget {
const LoginSettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context)!.settings),
),
body: ListView(
children: [
ClientCertificateFormField(onChanged: (certificate) {}),
],
),
);
}
}

View File

@@ -9,10 +9,11 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress";
final String? initialValue;
final void Function(String? address) onSubmit;
final ValueChanged<String?>? onChanged;
const ServerAddressFormField({
Key? key,
required this.onSubmit,
this.onChanged,
this.initialValue,
}) : super(key: key);
@@ -20,8 +21,10 @@ class ServerAddressFormField extends StatefulWidget {
State<ServerAddressFormField> createState() => _ServerAddressFormFieldState();
}
class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
class _ServerAddressFormFieldState extends State<ServerAddressFormField>
with AutomaticKeepAliveClientMixin {
bool _canClear = false;
final _textFieldKey = GlobalKey();
@override
void initState() {
@@ -38,10 +41,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
@override
Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<String>(
initialValue: widget.initialValue,
name: ServerAddressFormField.fkServerAddress,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: widget.onChanged,
builder: (field) {
return RawAutocomplete<String>(
focusNode: _focusNode,
@@ -51,6 +56,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
onSelected: onSelected,
options: options,
maxOptionsHeight: 200.0,
maxWidth: MediaQuery.sizeOf(context).width - 40,
);
},
key: const ValueKey('login-server-address'),
@@ -60,12 +66,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.where((element) => element.contains(textEditingValue.text));
},
onSelected: (option) {
_formatInput();
field.didChange(_textEditingController.text);
_formatInput(field);
},
fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
return TextFormField(
key: _textFieldKey,
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
@@ -78,15 +84,22 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
onPressed: () {
textEditingController.clear();
field.didChange(textEditingController.text);
widget.onSubmit(textEditingController.text);
},
)
: null,
),
autofocus: false,
onFieldSubmitted: (_) {
_formatInput(field);
onFieldSubmitted();
_formatInput();
},
onTapOutside: (event) {
if (!FocusScope.of(context).hasFocus) {
return;
}
_formatInput(field);
onFieldSubmitted();
FocusScope.of(context).unfocus();
},
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
@@ -113,7 +126,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
);
}
void _formatInput() {
void _formatInput(FormFieldState<String> field) {
String address = _textEditingController.text.trim();
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
_textEditingController.text = address;
@@ -121,8 +134,11 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
baseOffset: address.length,
extentOffset: address.length,
);
widget.onSubmit(address);
field.didChange(_textEditingController.text);
}
@override
bool get wantKeepAlive => true;
}
/// Taken from [Autocomplete]
@@ -131,12 +147,14 @@ class _AutocompleteOptions extends StatelessWidget {
required this.onSelected,
required this.options,
required this.maxOptionsHeight,
required this.maxWidth,
});
final AutocompleteOnSelected<String> onSelected;
final Iterable<String> options;
final double maxOptionsHeight;
final double maxWidth;
@override
Widget build(BuildContext context) {
@@ -145,7 +163,10 @@ class _AutocompleteOptions extends StatelessWidget {
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
constraints: BoxConstraints(
maxHeight: maxOptionsHeight,
maxWidth: maxWidth,
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,

View File

@@ -12,13 +12,13 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class UserCredentialsFormField extends StatefulWidget {
static const fkCredentials = 'credentials';
final void Function() onFieldsSubmitted;
final VoidCallback? onFieldsSubmitted;
final String? initialUsername;
final String? initialPassword;
final GlobalKey<FormBuilderState> formKey;
const UserCredentialsFormField({
Key? key,
required this.onFieldsSubmitted,
this.onFieldsSubmitted,
this.initialUsername,
this.initialPassword,
required this.formKey,
@@ -29,12 +29,14 @@ class UserCredentialsFormField extends StatefulWidget {
_UserCredentialsFormFieldState();
}
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField>
with AutomaticKeepAliveClientMixin {
final _usernameFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
@override
Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<LoginFormCredentials?>(
initialValue: LoginFormCredentials(
password: widget.initialPassword,
@@ -87,7 +89,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
LoginFormCredentials(password: password),
),
onFieldSubmitted: (_) {
widget.onFieldsSubmitted();
widget.onFieldsSubmitted?.call();
},
validator: (value) {
if (value?.trim().isEmpty ?? true) {
@@ -100,6 +102,9 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
),
);
}
@override
bool get wantKeepAlive => true;
}
/**

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routing/routes/app_logs_route.dart';
import 'package:paperless_mobile/theme.dart';
class LoginTransitionPage extends StatelessWidget {
@@ -19,13 +21,28 @@ class LoginTransitionPage extends StatelessWidget {
child: Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Align(
alignment: Alignment.bottomCenter,
child: Text(text).paddedOnly(bottom: 24),
),
],
),
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
child: Text(S.of(context)!.appLogs('')),
onPressed: () {
AppLogsRoute().push(context);
},
),
),
],
).padded(16),
),
),

View File

@@ -13,17 +13,14 @@ class SavedViewCubit extends Cubit<SavedViewState> {
SavedViewCubit(this._savedViewRepository)
: super(const SavedViewState.initial()) {
_savedViewRepository.addListener(
this,
onChanged: (views) {
views.when(
initial: (savedViews) => emit(const SavedViewState.initial()),
loading: (savedViews) => emit(const SavedViewState.loading()),
loaded: (savedViews) =>
emit(SavedViewState.loaded(savedViews: savedViews)),
error: (savedViews) => emit(const SavedViewState.error()),
);
},
_savedViewRepository.addListener(_onSavedViewsChanged);
}
void _onSavedViewsChanged() {
emit(
SavedViewState.loaded(
savedViews: _savedViewRepository.savedViews,
),
);
}
@@ -53,7 +50,7 @@ class SavedViewCubit extends Cubit<SavedViewState> {
@override
Future<void> close() {
_savedViewRepository.removeListener(this);
_savedViewRepository.removeListener(_onSavedViewsChanged);
return super.close();
}
}

View File

@@ -34,32 +34,13 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
required this.savedView,
int initialCount = 25,
}) : super(
SavedViewDetailsState(
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
tags: _labelRepository.state.tags,
storagePaths: _labelRepository.state.storagePaths,
viewType: _userState.savedViewsViewType,
),
SavedViewDetailsState(viewType: _userState.savedViewsViewType),
) {
notifier.addListener(
this,
onDeleted: remove,
onUpdated: replace,
);
_labelRepository.addListener(
this,
onChanged: (labels) {
if (!isClosed) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
));
}
},
);
updateFilter(
filter: savedView.toDocumentFilter().copyWith(
page: 1,

View File

@@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';

View File

@@ -3,6 +3,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
@@ -17,15 +18,12 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
@override
final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override
final DocumentChangedNotifier notifier;
SimilarDocumentsCubit(
this.api,
this.notifier,
this._labelRepository,
this.connectivityStatusService, {
required this.documentId,
}) : super(const SimilarDocumentsState(filter: DocumentFilter())) {
@@ -39,19 +37,30 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
@override
Future<void> initialize() async {
if (!state.hasLoaded) {
try {
await updateFilter(
filter: state.filter.copyWith(
moreLike: () => documentId,
sortField: SortField.score,
),
);
emit(state.copyWith(error: null));
} on PaperlessApiException catch (e, stackTrace) {
logger.fe(
"An error occurred while loading similar documents for document $documentId",
className: "SimilarDocumentsCubit",
methodName: "initialize",
error: e.details,
stackTrace: stackTrace,
);
emit(state.copyWith(error: e.code));
}
}
}
@override
Future<void> close() {
notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close();
}

View File

@@ -1,19 +1,22 @@
part of 'similar_documents_cubit.dart';
class SimilarDocumentsState extends DocumentPagingState {
final ErrorCode? error;
const SimilarDocumentsState({
required super.filter,
super.hasLoaded,
super.isLoading,
super.value,
this.error,
});
@override
List<Object> get props => [
List<Object?> get props => [
filter,
hasLoaded,
isLoading,
value,
error,
];
@override
@@ -36,12 +39,14 @@ class SimilarDocumentsState extends DocumentPagingState {
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
ErrorCode? error,
}) {
return SimilarDocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded,
isLoading: isLoading ?? this.isLoading,
value: value ?? this.value,
filter: filter ?? this.filter,
error: error,
);
}
}

View File

@@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
@@ -49,6 +51,16 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
child: OfflineWidget(),
);
}
if (state.error != null) {
return SliverFillRemaining(
child: Center(
child: Text(
translateError(context, state.error!),
textAlign: TextAlign.center,
),
).padded(),
);
}
if (state.hasLoaded &&
!state.isLoading &&
state.documents.isEmpty) {

View File

@@ -1010,14 +1010,19 @@
"couldNotLoadLogfileFrom": "No es pot carregar log desde {date}.",
"loadingLogsFrom": "Carregant registres des de {date}...",
"clearLogs": "Netejar registres des de {date}",
"showPdf": "Show PDF",
"showPdf": "Mostra PDF",
"@showPdf": {
"description": "Tooltip shown on the \"show pdf\" button on the document edit page"
},
"hidePdf": "Hide PDF",
"hidePdf": "Oculta PDF",
"@hidePdf": {
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"misc": "Miscel·lanni",
"loggingOut": "Sortint...",
"testingConnection": "Provant connexió...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Versió {versionCode}"
}

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"loggingOut": "Logging out...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Sonstige",
"loggingOut": "Abmelden..."
"loggingOut": "Abmelden...",
"testingConnection": "Teste Verbindung...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"loggingOut": "Logging out...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -873,7 +873,7 @@
"@donate": {
"description": "Label of the in-app donate button"
},
"donationDialogContent": "¡Gracias por querer apoyar esta aplicación!\nDebido a las políticas de pago, tanto de Google como de Apple, no se puede mostrar ningún enlace que lo dirija a las donaciones. En este contexto, ni siquiera es posible enlazar la página del repositorio del proyecto. Por lo tanto, puedes visitar la sección \"Donations\" en el archivo README de este proyecto. Tu apoyo es valorado gratamente y ayuda a mantener con vida el desarrollo de esta aplicación.\n¡Muchas gracias!",
"donationDialogContent": "¡Gracias por querer apoyar esta aplicación!\nDebido a las políticas de pago, tanto de Google como de Apple, no se puede mostrar ningún enlace que lo dirija a las donaciones. En este contexto, ni siquiera es posible enlazar la página del repositorio del proyecto. Por lo tanto, puedes visitar la sección \"Donaciones\" en el archivo README de este proyecto. Tu apoyo es valorado gratamente y ayuda a mantener con vida el desarrollo de esta aplicación.\n¡Muchas gracias!",
"@donationDialogContent": {
"description": "Text displayed in the donation dialog"
},
@@ -881,11 +881,11 @@
"@noDocumentsFound": {
"description": "Message shown when no documents were found."
},
"couldNotDeleteCorrespondent": "No se pudo remover el interlocutor, intente nuevamente.",
"couldNotDeleteCorrespondent": "No se pudo borrar el interlocutor, intente nuevamente.",
"@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted."
},
"couldNotDeleteDocumentType": "No se pudo remover el tipo de documento, intente nuevamente.",
"couldNotDeleteDocumentType": "No se pudo borrar el tipo de documento, intente nuevamente.",
"@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted"
},
@@ -893,7 +893,7 @@
"@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted"
},
"couldNotDeleteStoragePath": "No se pudo remover la ruta de almacenamiento, intente nuevamente.",
"couldNotDeleteStoragePath": "No se pudo borrar la ruta de almacenamiento, intente nuevamente.",
"@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted"
},
@@ -934,7 +934,7 @@
"description": "Message shown when a saved view could not be updated"
},
"couldNotUpdateStoragePath": "No se pudo actualizar la ruta de almacenamiento, intente nuevamente.",
"savedViewSuccessfullyUpdated": "La vista guardada se actualizó correctamente.",
"savedViewSuccessfullyUpdated": "Vista guardada actualizada correctamente.",
"@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated."
},
@@ -984,7 +984,7 @@
"@authenticatingDots": {
"description": "Message shown when the app is authenticating the user"
},
"persistingUserInformation": "Preservando información del usuario...",
"persistingUserInformation": "Guardando información del usuario...",
"fetchingUserInformation": "Obteniendo información del usuario...",
"@fetchingUserInformation": {
"description": "Message shown when the app loads user data from the server"
@@ -1001,7 +1001,7 @@
"@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes."
},
"changelog": "Changelog",
"changelog": "Registro de cambios",
"noLogsFoundOn": "No se encontraron registros en {date}.",
"logfileBottomReached": "Has alcanzado el final del archivo de registro.",
"appLogs": "Registros de la aplicación {date}",
@@ -1010,14 +1010,19 @@
"couldNotLoadLogfileFrom": "No se pudo cargar el archivo de registro desde {date}.",
"loadingLogsFrom": "Cargando registros desde {date}...",
"clearLogs": "Limpiar registros desde {date}",
"showPdf": "Show PDF",
"showPdf": "Mostrar PDF",
"@showPdf": {
"description": "Tooltip shown on the \"show pdf\" button on the document edit page"
},
"hidePdf": "Hide PDF",
"hidePdf": "Ocultar PDF",
"@hidePdf": {
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"misc": "Otros",
"loggingOut": "Cerrando sesión...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Sonstige",
"loggingOut": "Logging out..."
"loggingOut": "Logging out...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

1028
lib/l10n/intl_nl.arb Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"loggingOut": "Logging out...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"loggingOut": "Logging out...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -1019,5 +1019,10 @@
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
},
"misc": "Miscellaneous",
"loggingOut": "Logging out..."
"loggingOut": "Logging out...",
"testingConnection": "Testing connection...",
"@testingConnection": {
"description": "Text shown while the app tries to establish a connection to the specified host."
},
"version": "Version {versionCode}"
}

View File

@@ -22,6 +22,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/accessibility/accessible_page.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -123,6 +124,7 @@ Future<void> _initHive() async {
void main() async {
runZonedGuarded(() async {
Bloc.observer = MyBlocObserver();
WidgetsFlutterBinding.ensureInitialized();
await FileService.instance.initialize();
@@ -371,6 +373,16 @@ class _GoRouterShellState extends State<GoRouterShell> {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return MaterialApp.router(
builder: (context, child) {
return AnnotatedRegion<SystemUiOverlayStyle>(
child: child!,
value: buildOverlayStyle(
Theme.of(context),
systemNavigationBarColor:
Theme.of(context).colorScheme.background,
),
);
},
routerConfig: _router,
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
@@ -53,7 +54,6 @@ class DocumentDetailsRoute extends GoRouteData {
context.read(),
context.read(),
context.read(),
context.read(),
id: id,
)..initialize(),
lazy: false,
@@ -131,9 +131,9 @@ class BulkEditDocumentsRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
final labelRepository = context.read<LabelRepository>();
return BlocProvider(
create: (_) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
selection: $extra.selection,
@@ -144,9 +144,9 @@ class BulkEditDocumentsRoute extends GoRouteData {
LabelType.tag => const FullscreenBulkEditTagsWidget(),
_ => FullscreenBulkEditLabelPage(
options: switch ($extra.type) {
LabelType.correspondent => state.correspondents,
LabelType.documentType => state.documentTypes,
LabelType.storagePath => state.storagePaths,
LabelType.correspondent => labelRepository.correspondents,
LabelType.documentType => labelRepository.documentTypes,
LabelType.storagePath => labelRepository.storagePaths,
_ => throw Exception("Parameter not allowed here."),
},
selection: state.selection,

View File

@@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
import 'package:paperless_mobile/routing/navigation_keys.dart';
class LabelsBranch extends StatefulShellBranchData {
static final GlobalKey<NavigatorState> $navigatorKey = labelsNavigatorKey;
const LabelsBranch();
@@ -81,7 +82,6 @@ class LinkedDocumentsRoute extends GoRouteData {
context.read(),
context.read(),
context.read(),
context.read(),
),
child: const LinkedDocumentsPage(),
);

View File

@@ -0,0 +1,9 @@
enum CustomFieldDataType {
text,
boolean,
date,
url,
integer,
number,
monetary;
}

View File

@@ -0,0 +1,26 @@
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/src/models/custom_field_data_type.dart';
part 'custom_field_model.g.dart';
@JsonSerializable()
class CustomFieldModel with EquatableMixin {
final int? id;
final String name;
final CustomFieldDataType dataType;
CustomFieldModel({
this.id,
required this.name,
required this.dataType,
});
@override
List<Object?> get props => [id, name, dataType];
factory CustomFieldModel.fromJson(Map<String, dynamic> json) =>
_$CustomFieldModelFromJson(json);
Map<String, dynamic> toJson() => _$CustomFieldModelToJson(this);
}

View File

@@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/converters/local_date_time_json_converter.dart';
import 'package:paperless_api/src/models/custom_field_model.dart';
import 'package:paperless_api/src/models/search_hit.dart';
part 'document_model.g.dart';
@@ -50,6 +51,7 @@ class DocumentModel extends Equatable {
// Only present if full_perms=true
final Permissions? permissions;
final Iterable<CustomFieldModel>? customFields;
const DocumentModel({
required this.id,
@@ -69,6 +71,7 @@ class DocumentModel extends Equatable {
this.owner,
this.userCanChange,
this.permissions,
this.customFields,
});
factory DocumentModel.fromJson(Map<String, dynamic> json) =>
@@ -89,6 +92,8 @@ class DocumentModel extends Equatable {
int? Function()? archiveSerialNumber,
String? originalFileName,
String? archivedFileName,
int? Function()? owner,
bool? userCanChange,
}) {
return DocumentModel(
id: id,
@@ -107,6 +112,8 @@ class DocumentModel extends Equatable {
? archiveSerialNumber()
: this.archiveSerialNumber,
archivedFileName: archivedFileName ?? this.archivedFileName,
owner: owner != null ? owner() : this.owner,
userCanChange: userCanChange ?? this.userCanChange,
);
}
@@ -114,17 +121,18 @@ class DocumentModel extends Equatable {
List<Object?> get props => [
id,
title,
content.hashCode,
tags,
documentType,
storagePath,
content,
correspondent,
documentType,
tags,
storagePath,
created,
modified,
added,
archiveSerialNumber,
originalFileName,
archivedFileName,
storagePath,
owner,
userCanChange,
];
}

View File

@@ -19,10 +19,13 @@ class PaperlessFormValidationException implements Exception {
return validationMessages[formKey];
}
static bool canParse(Map<String, dynamic> json) {
static bool canParse(dynamic json) {
if (json is Map<String, dynamic>) {
return json.values
.every((element) => element is String || element is List);
}
return false;
}
factory PaperlessFormValidationException.fromJson(Map<String, dynamic> json) {
final Map<String, String> validationMessages = {};

View File

@@ -11,9 +11,8 @@ class PaperlessServerMessageException implements Exception {
static bool canParse(dynamic json) {
if (json is Map<String, dynamic>) {
return json.containsKey('detail') && json.length == 1;
} else {
return false;
}
return false;
}
factory PaperlessServerMessageException.fromJson(Map<String, dynamic> json) =>

View File

@@ -68,5 +68,8 @@ enum ErrorCode {
loadTasksError,
userNotFound,
userAlreadyExists,
updateSavedViewError;
updateSavedViewError,
customFieldCreateFailed,
customFieldLoadFailed,
customFieldDeleteFailed;
}

View File

@@ -14,9 +14,15 @@ class PaperlessServerStatisticsModel {
: documentsTotal = json['documents_total'] ?? 0,
documentsInInbox = json['documents_inbox'] ?? 0,
totalChars = json["character_count"],
fileTypeCounts = (json['document_file_type_counts'] as List? ?? [])
.map((e) => DocumentFileTypeCount.fromJson(e))
.toList();
fileTypeCounts =
_parseFileTypeCounts(json['document_file_type_counts']);
static List<DocumentFileTypeCount> _parseFileTypeCounts(dynamic value) {
if (value is List) {
return value.map((e) => DocumentFileTypeCount.fromJson(e)).toList();
}
return [];
}
}
class DocumentFileTypeCount {

View File

@@ -0,0 +1,8 @@
import 'package:paperless_api/src/models/custom_field_model.dart';
abstract interface class CustomFieldsApi {
Future<CustomFieldModel> createCustomField(CustomFieldModel customField);
Future<CustomFieldModel?> getCustomField(int id);
Future<List<CustomFieldModel>> getCustomFields();
Future<int> deleteCustomField(CustomFieldModel customField);
}

View File

@@ -0,0 +1,72 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/custom_field_model.dart';
import 'package:paperless_api/src/modules/custom_fields/custom_fields_api.dart';
import 'package:paperless_api/src/request_utils.dart';
class CustomFieldsApiImpl implements CustomFieldsApi {
final Dio _dio;
const CustomFieldsApiImpl(this._dio);
@override
Future<CustomFieldModel> createCustomField(
CustomFieldModel customField) async {
try {
final response = await _dio.post(
"/api/custom_fields/",
data: customField.toJson(),
options: Options(
validateStatus: (status) => status == 201,
),
);
return CustomFieldModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.customFieldCreateFailed,
),
);
}
}
@override
Future<int> deleteCustomField(CustomFieldModel customField) async {
try {
await _dio.delete(
"/api/custom_fields/${customField.id}/",
options: Options(
validateStatus: (status) => status == 204,
),
);
return customField.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.customFieldDeleteFailed,
),
);
}
}
@override
Future<CustomFieldModel?> getCustomField(int id) {
return getSingleResult(
'/api/custom_fields/$id/',
CustomFieldModel.fromJson,
ErrorCode.customFieldLoadFailed,
client: _dio,
);
}
@override
Future<List<CustomFieldModel>> getCustomFields() {
return getCollection(
'/api/custom_fields/?page=1&page_size=100000',
CustomFieldModel.fromJson,
ErrorCode.customFieldLoadFailed,
client: _dio,
);
}
}

View File

@@ -109,7 +109,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentLoadFailed),
orElse: PaperlessApiException(
ErrorCode.documentLoadFailed,
details: exception.message,
),
);
}
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/models.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/request_utils.dart';

Some files were not shown because too many files have changed in this diff Show More