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 * Neue Einstellung um Animationen zu deaktivieren
* Verbesserte Validierung von Server-Adressen * 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 * Add setting to disable animations
* Improved server-address validation * 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, type: DioExceptionType.badResponse,
), ),
); );
} else if (data is String && } else if (data is String) {
data.contains("No required SSL certificate was sent")) { if (data.contains("No required SSL certificate was sent")) {
handler.reject( handler.reject(
DioException( DioException(
requestOptions: err.requestOptions, requestOptions: err.requestOptions,
type: DioExceptionType.badResponse, type: DioExceptionType.badResponse,
error: error: const PaperlessApiException(
const PaperlessApiException(ErrorCode.missingClientCertificate), 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 { } else {
handler.reject(err); handler.reject(err);
} }

View File

@@ -1,192 +1,190 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.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; 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([ await Future.wait([
findAllCorrespondents(), if (loadCorrespondents) findAllCorrespondents(),
findAllDocumentTypes(), if (loadDocumentTypes) findAllDocumentTypes(),
findAllStoragePaths(), if (loadStoragePaths) findAllStoragePaths(),
findAllTags(), if (loadTags) findAllTags(),
]); ]);
} }
Future<Tag> createTag(Tag object) async { Future<Tag> createTag(Tag object) async {
final created = await _api.saveTag(object); final created = await _api.saveTag(object);
final updatedState = {...state.tags} tags = {...tags, created.id!: created};
..putIfAbsent(created.id!, () => created); notifyListeners();
emit(state.copyWith(tags: updatedState));
return created; return created;
} }
Future<int> deleteTag(Tag tag) async { Future<int> deleteTag(Tag tag) async {
await _api.deleteTag(tag); await _api.deleteTag(tag);
final updatedState = {...state.tags}..removeWhere((k, v) => k == tag.id); tags.remove(tag.id!);
emit(state.copyWith(tags: updatedState)); notifyListeners();
return tag.id!; return tag.id!;
} }
Future<Tag?> findTag(int id) async { Future<Tag?> findTag(int id) async {
final tag = await _api.getTag(id); final tag = await _api.getTag(id);
if (tag != null) { if (tag != null) {
final updatedState = {...state.tags}..[id] = tag; tags = {...tags, id: tag};
emit(state.copyWith(tags: updatedState)); notifyListeners();
return tag; return tag;
} }
return null; return null;
} }
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async { Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids); final data = await _api.getTags(ids);
final updatedState = {...state.tags} tags = {for (var tag in data) tag.id!: tag};
..addEntries(tags.map((e) => MapEntry(e.id!, e))); notifyListeners();
emit(state.copyWith(tags: updatedState)); return data;
return tags;
} }
Future<Tag> updateTag(Tag tag) async { Future<Tag> updateTag(Tag tag) async {
final updated = await _api.updateTag(tag); final updated = await _api.updateTag(tag);
final updatedState = {...state.tags}..update(updated.id!, (_) => updated); tags = {...tags, updated.id!: updated};
emit(state.copyWith(tags: updatedState)); notifyListeners();
return updated; return updated;
} }
Future<Correspondent> createCorrespondent(Correspondent correspondent) async { Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent); final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.correspondents} correspondents = {...correspondents, created.id!: created};
..putIfAbsent(created.id!, () => created); notifyListeners();
emit(state.copyWith(correspondents: updatedState));
return created; return created;
} }
Future<int> deleteCorrespondent(Correspondent correspondent) async { Future<int> deleteCorrespondent(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent); await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.correspondents} correspondents.remove(correspondent.id!);
..removeWhere((k, v) => k == correspondent.id); notifyListeners();
emit(state.copyWith(correspondents: updatedState));
return correspondent.id!; return correspondent.id!;
} }
Future<Correspondent?> findCorrespondent(int id) async { Future<Correspondent?> findCorrespondent(int id) async {
final correspondent = await _api.getCorrespondent(id); final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) { if (correspondent != null) {
final updatedState = {...state.correspondents}..[id] = correspondent; correspondents = {...correspondents, id: correspondent};
emit(state.copyWith(correspondents: updatedState)); notifyListeners();
return correspondent; return correspondent;
} }
return null; return null;
} }
Future<Iterable<Correspondent>> findAllCorrespondents( Future<Iterable<Correspondent>> findAllCorrespondents() async {
[Iterable<int>? ids]) async { final data = await _api.getCorrespondents();
final correspondents = await _api.getCorrespondents(ids); correspondents = {for (var element in data) element.id!: element};
final updatedState = { notifyListeners();
...state.correspondents, return data;
}..addAll({for (var element in correspondents) element.id!: element});
emit(state.copyWith(correspondents: updatedState));
return correspondents;
} }
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async { Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent); final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.correspondents} correspondents = {...correspondents, updated.id!: updated};
..update(updated.id!, (_) => updated); notifyListeners();
emit(state.copyWith(correspondents: updatedState));
return updated; return updated;
} }
Future<DocumentType> createDocumentType(DocumentType documentType) async { Future<DocumentType> createDocumentType(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType); final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.documentTypes} documentTypes = {...documentTypes, created.id!: created};
..putIfAbsent(created.id!, () => created); notifyListeners();
emit(state.copyWith(documentTypes: updatedState));
return created; return created;
} }
Future<int> deleteDocumentType(DocumentType documentType) async { Future<int> deleteDocumentType(DocumentType documentType) async {
await _api.deleteDocumentType(documentType); await _api.deleteDocumentType(documentType);
final updatedState = {...state.documentTypes} documentTypes.remove(documentType.id!);
..removeWhere((k, v) => k == documentType.id); notifyListeners();
emit(state.copyWith(documentTypes: updatedState));
return documentType.id!; return documentType.id!;
} }
Future<DocumentType?> findDocumentType(int id) async { Future<DocumentType?> findDocumentType(int id) async {
final documentType = await _api.getDocumentType(id); final documentType = await _api.getDocumentType(id);
if (documentType != null) { if (documentType != null) {
final updatedState = {...state.documentTypes}..[id] = documentType; documentTypes = {...documentTypes, id: documentType};
emit(state.copyWith(documentTypes: updatedState)); notifyListeners();
return documentType; return documentType;
} }
return null; return null;
} }
Future<Iterable<DocumentType>> findAllDocumentTypes( Future<Iterable<DocumentType>> findAllDocumentTypes() async {
[Iterable<int>? ids]) async { final documentTypes = await _api.getDocumentTypes();
final documentTypes = await _api.getDocumentTypes(ids); this.documentTypes = {
final updatedState = {...state.documentTypes} for (var dt in documentTypes) dt.id!: dt,
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); };
emit(state.copyWith(documentTypes: updatedState)); notifyListeners();
return documentTypes; return documentTypes;
} }
Future<DocumentType> updateDocumentType(DocumentType documentType) async { Future<DocumentType> updateDocumentType(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType); final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.documentTypes} documentTypes = {...documentTypes, updated.id!: updated};
..update(updated.id!, (_) => updated); notifyListeners();
emit(state.copyWith(documentTypes: updatedState));
return updated; return updated;
} }
Future<StoragePath> createStoragePath(StoragePath storagePath) async { Future<StoragePath> createStoragePath(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath); final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.storagePaths} storagePaths = {...storagePaths, created.id!: created};
..putIfAbsent(created.id!, () => created); notifyListeners();
emit(state.copyWith(storagePaths: updatedState));
return created; return created;
} }
Future<int> deleteStoragePath(StoragePath storagePath) async { Future<int> deleteStoragePath(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath); await _api.deleteStoragePath(storagePath);
final updatedState = {...state.storagePaths} storagePaths.remove(storagePath.id!);
..removeWhere((k, v) => k == storagePath.id); notifyListeners();
emit(state.copyWith(storagePaths: updatedState));
return storagePath.id!; return storagePath.id!;
} }
Future<StoragePath?> findStoragePath(int id) async { Future<StoragePath?> findStoragePath(int id) async {
final storagePath = await _api.getStoragePath(id); final storagePath = await _api.getStoragePath(id);
if (storagePath != null) { if (storagePath != null) {
final updatedState = {...state.storagePaths}..[id] = storagePath; storagePaths = {...storagePaths, id: storagePath};
emit(state.copyWith(storagePaths: updatedState)); notifyListeners();
return storagePath; return storagePath;
} }
return null; return null;
} }
Future<Iterable<StoragePath>> findAllStoragePaths( Future<Iterable<StoragePath>> findAllStoragePaths() async {
[Iterable<int>? ids]) async { final storagePaths = await _api.getStoragePaths();
final storagePaths = await _api.getStoragePaths(ids); this.storagePaths = {
final updatedState = {...state.storagePaths} for (var sp in storagePaths) sp.id!: sp,
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); };
emit(state.copyWith(storagePaths: updatedState)); notifyListeners();
return storagePaths; return storagePaths;
} }
Future<StoragePath> updateStoragePath(StoragePath storagePath) async { Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath); final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.storagePaths} storagePaths = {...storagePaths, updated.id!: updated};
..update(updated.id!, (_) => updated); notifyListeners();
emit(state.copyWith(storagePaths: updatedState));
return updated; 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 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter/cupertino.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
part 'saved_view_repository_state.dart'; class SavedViewRepository extends ChangeNotifier {
part 'saved_view_repository.g.dart';
part 'saved_view_repository.freezed.dart';
class SavedViewRepository
extends PersistentRepository<SavedViewRepositoryState> {
final PaperlessSavedViewsApi _api; final PaperlessSavedViewsApi _api;
final Completer _initialized = Completer(); Map<int, SavedView> savedViews = {};
SavedViewRepository(this._api) SavedViewRepository(this._api);
: super(const SavedViewRepositoryState.initial());
Future<void> initialize() async { Future<void> initialize() async {
try { await findAll();
await findAll();
_initialized.complete();
} catch (e) {
_initialized.completeError(e);
emit(const SavedViewRepositoryState.error());
}
} }
Future<SavedView> create(SavedView object) async { Future<SavedView> create(SavedView object) async {
await _initialized.future;
final created = await _api.save(object); final created = await _api.save(object);
final updatedState = {...state.savedViews} savedViews = {...savedViews, created.id!: created};
..putIfAbsent(created.id!, () => created); notifyListeners();
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
return created; return created;
} }
Future<SavedView> update(SavedView object) async { Future<SavedView> update(SavedView object) async {
await _initialized.future;
final updated = await _api.update(object); final updated = await _api.update(object);
final updatedState = {...state.savedViews}..update( savedViews = {...savedViews, updated.id!: updated};
updated.id!, notifyListeners();
(_) => updated,
ifAbsent: () => updated,
);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
return updated; return updated;
} }
Future<int> delete(SavedView view) async { Future<int> delete(SavedView view) async {
await _initialized.future;
await _api.delete(view); await _api.delete(view);
final updatedState = {...state.savedViews}..remove(view.id); savedViews.remove(view.id!);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); notifyListeners();
return view.id!; return view.id!;
} }
Future<SavedView?> find(int id) async { Future<SavedView?> find(int id) async {
await _initialized.future;
final found = await _api.find(id); final found = await _api.find(id);
if (found != null) { if (found != null) {
final updatedState = {...state.savedViews} savedViews = {...savedViews, id: found};
..update(id, (_) => found, ifAbsent: () => found); notifyListeners();
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
} }
return found; return found;
} }
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async { Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids); final found = await _api.findAll(ids);
final updatedState = { savedViews = {
...state.savedViews, for (final view in found) view.id!: view,
...{for (final view in found) view.id!: view},
}; };
emit(SavedViewRepositoryState.loaded(savedViews: updatedState)); notifyListeners();
return found; return found;
} }
// @override
// Future<void> clear() async {
// await _initialized.future;
// await super.clear();
// emit(const SavedViewRepositoryState.initial());
// }
// @override
// SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
// return SavedViewRepositoryState.fromJson(json);
// }
// @override
// Map<String, dynamic>? toJson(SavedViewRepositoryState state) {
// return state.toJson();
// }
} }

View File

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

View File

@@ -1,30 +1,45 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart'; import 'package:paperless_mobile/core/repository/persistent_repository.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
part 'user_repository_state.dart'; part 'user_repository_state.dart';
/// Repository for new users (API v3, server version 1.14.2+)
class UserRepository extends PersistentRepository<UserRepositoryState> { 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 { Future<void> initialize() async {
await findAll(); await findAll();
} }
Future<Iterable<UserModel>> findAll() async { Future<Iterable<UserModel>> findAll() async {
final users = await _userApiV3.findAll(); if (_userApi is PaperlessUserApiV3Impl) {
emit(state.copyWith(users: {for (var e in users) e.id: e})); final users = await (_userApi as PaperlessUserApiV3Impl).findAll();
return users; 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 { Future<UserModel?> find(int id) async {
final user = await _userApiV3.find(id); if (_userApi is PaperlessUserApiV3Impl) {
emit(state.copyWith(users: state.users..[id] = user)); final user = await (_userApi as PaperlessUserApiV3Impl).find(id);
return user; 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 // @override

View File

@@ -14,12 +14,12 @@ class FileService {
static FileService? _singleton; static FileService? _singleton;
late final Directory _logDirectory; late Directory _logDirectory;
late final Directory _temporaryDirectory; late Directory _temporaryDirectory;
late final Directory _documentsDirectory; late Directory _documentsDirectory;
late final Directory _downloadsDirectory; late Directory _downloadsDirectory;
late final Directory _uploadDirectory; late Directory _uploadDirectory;
late final Directory _temporaryScansDirectory; late Directory _temporaryScansDirectory;
Directory get logDirectory => _logDirectory; Directory get logDirectory => _logDirectory;
Directory get temporaryDirectory => _temporaryDirectory; Directory get temporaryDirectory => _temporaryDirectory;
@@ -186,14 +186,15 @@ class FileService {
} }
Future<void> _initTemporaryDirectory() async { Future<void> _initTemporaryDirectory() async {
_temporaryDirectory = await getTemporaryDirectory(); _temporaryDirectory =
await getTemporaryDirectory().then((value) => value.create());
} }
Future<void> _initializeDocumentsDirectory() async { Future<void> _initializeDocumentsDirectory() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
final dirs = final dirs =
await getExternalStorageDirectories(type: StorageDirectory.documents); await getExternalStorageDirectories(type: StorageDirectory.documents);
_documentsDirectory = dirs!.first; _documentsDirectory = await dirs!.first.create(recursive: true);
return; return;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
@@ -212,12 +213,12 @@ class FileService {
.then((directory) async => .then((directory) async =>
directory?.firstOrNull ?? directory?.firstOrNull ??
await getApplicationDocumentsDirectory()) await getApplicationDocumentsDirectory())
.then((directory) => .then((directory) => Directory(p.join(directory.path, 'logs'))
Directory('${directory.path}/logs').create(recursive: true)); .create(recursive: true));
return; return;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
_logDirectory = await getApplicationDocumentsDirectory().then( _logDirectory = await getApplicationDocumentsDirectory().then((value) =>
(value) => Directory('${value.path}/logs').create(recursive: true)); Directory(p.join(value.path, 'logs')).create(recursive: true));
return; return;
} }
throw UnsupportedError("Platform not supported."); throw UnsupportedError("Platform not supported.");
@@ -246,7 +247,7 @@ class FileService {
Future<void> _initUploadDirectory() async { Future<void> _initUploadDirectory() async {
final dir = await getApplicationDocumentsDirectory() final dir = await getApplicationDocumentsDirectory()
.then((dir) => Directory('${dir.path}/upload')); .then((dir) => Directory(p.join(dir.path, 'upload')));
_uploadDirectory = await dir.create(recursive: true); _uploadDirectory = await dir.create(recursive: true);
} }
@@ -265,3 +266,13 @@ enum PaperlessDirectoryType {
upload, upload,
logs; 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.userNotFound => S.of(context)!.userNotFound,
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView, ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
ErrorCode.userAlreadyExists => S.of(context)!.userAlreadyExists, 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:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:paperless_api/paperless_api.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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.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_state.dart';
part 'document_bulk_action_cubit.freezed.dart';
class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> { class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
final PaperlessDocumentsApi _documentsApi; final PaperlessDocumentsApi _documentsApi;
final LabelRepository _labelRepository;
final DocumentChangedNotifier _notifier; final DocumentChangedNotifier _notifier;
DocumentBulkActionCubit( DocumentBulkActionCubit(
this._documentsApi, this._documentsApi,
this._labelRepository,
this._notifier, { this._notifier, {
required List<DocumentModel> selection, required List<DocumentModel> selection,
}) : super( }) : super(
DocumentBulkActionState( DocumentBulkActionState(
selection: selection, selection: selection,
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
), ),
) { ) {
_notifier.addListener( _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 { Future<void> bulkDelete() async {
@@ -69,47 +49,74 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
} }
Future<void> bulkModifyCorrespondent(int? correspondentId) async { Future<void> bulkModifyCorrespondent(int? correspondentId) async {
final modifiedDocumentIds = await _documentsApi.bulkAction( try {
BulkModifyLabelAction.correspondent( final modifiedDocumentIds = await _documentsApi.bulkAction(
state.selectedIds, BulkModifyLabelAction.correspondent(
labelId: correspondentId, state.selectedIds,
), labelId: correspondentId,
); ),
final updatedDocuments = state.selection );
.where((element) => modifiedDocumentIds.contains(element.id)) final updatedDocuments = state.selection
.map((doc) => doc.copyWith(correspondent: () => correspondentId)); .where((element) => modifiedDocumentIds.contains(element.id))
for (final doc in updatedDocuments) { .map((doc) => doc.copyWith(correspondent: () => correspondentId));
_notifier.notifyUpdated(doc); 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 { Future<void> bulkModifyDocumentType(int? documentTypeId) async {
final modifiedDocumentIds = await _documentsApi.bulkAction( try {
BulkModifyLabelAction.documentType( final modifiedDocumentIds = await _documentsApi.bulkAction(
state.selectedIds, BulkModifyLabelAction.documentType(
labelId: documentTypeId, state.selectedIds,
), labelId: documentTypeId,
); ),
final updatedDocuments = state.selection );
.where((element) => modifiedDocumentIds.contains(element.id)) final updatedDocuments = state.selection
.map((doc) => doc.copyWith(documentType: () => documentTypeId)); .where((element) => modifiedDocumentIds.contains(element.id))
for (final doc in updatedDocuments) { .map((doc) => doc.copyWith(documentType: () => documentTypeId));
_notifier.notifyUpdated(doc); 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 { Future<void> bulkModifyStoragePath(int? storagePathId) async {
final modifiedDocumentIds = await _documentsApi.bulkAction( try {
BulkModifyLabelAction.storagePath( final modifiedDocumentIds = await _documentsApi.bulkAction(
state.selectedIds, BulkModifyLabelAction.storagePath(
labelId: storagePathId, state.selectedIds,
), labelId: storagePathId,
); ),
final updatedDocuments = state.selection );
.where((element) => modifiedDocumentIds.contains(element.id)) final updatedDocuments = state.selection
.map((doc) => doc.copyWith(storagePath: () => storagePathId)); .where((element) => modifiedDocumentIds.contains(element.id))
for (final doc in updatedDocuments) { .map((doc) => doc.copyWith(storagePath: () => storagePathId));
_notifier.notifyUpdated(doc); for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
} }
} }
@@ -117,28 +124,36 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
Iterable<int> addTagIds = const [], Iterable<int> addTagIds = const [],
Iterable<int> removeTagIds = const [], Iterable<int> removeTagIds = const [],
}) async { }) async {
final modifiedDocumentIds = await _documentsApi.bulkAction( try {
BulkModifyTagsAction( final modifiedDocumentIds = await _documentsApi.bulkAction(
state.selectedIds, BulkModifyTagsAction(
addTags: addTagIds, state.selectedIds,
removeTags: removeTagIds, addTags: addTagIds,
), removeTags: removeTagIds,
); ),
final updatedDocuments = state.selection );
.where((element) => modifiedDocumentIds.contains(element.id)) final updatedDocuments = state.selection
.map((doc) => doc.copyWith(tags: [ .where((element) => modifiedDocumentIds.contains(element.id))
...doc.tags.toSet().difference(removeTagIds.toSet()), .map((doc) => doc.copyWith(tags: [
...addTagIds ...doc.tags.toSet().difference(removeTagIds.toSet()),
])); ...addTagIds
for (final doc in updatedDocuments) { ]));
_notifier.notifyUpdated(doc); for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
} }
} }
@override @override
Future<void> close() { Future<void> close() {
_notifier.removeListener(this); _notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }
} }

View File

@@ -1,15 +1,18 @@
part of 'document_bulk_action_cubit.dart'; part of 'document_bulk_action_cubit.dart';
@freezed class DocumentBulkActionState {
class DocumentBulkActionState with _$DocumentBulkActionState { final List<DocumentModel> selection;
const DocumentBulkActionState._();
const factory DocumentBulkActionState({ DocumentBulkActionState({
required List<DocumentModel> selection, required this.selection,
required Map<int, Correspondent> correspondents, });
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _DocumentBulkActionState;
Iterable<int> get selectedIds => selection.map((d) => d.id); 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:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.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/widgets/form_fields/fullscreen_selection_form.dart';
import 'package:paperless_mobile/core/extensions/dart_extensions.dart'; import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
@@ -35,11 +36,12 @@ class _FullscreenBulkEditTagsWidgetState
void initState() { void initState() {
super.initState(); super.initState();
final state = context.read<DocumentBulkActionCubit>().state; final state = context.read<DocumentBulkActionCubit>().state;
final labels = context.read<LabelRepository>();
_sharedTags = state.selection _sharedTags = state.selection
.map((e) => e.tags) .map((e) => e.tags)
.map((e) => e.toSet()) .map((e) => e.toSet())
.fold( .fold(
state.tags.values.map((e) => e.id!).toSet(), labels.tags.values.map((e) => e.id!).toSet(),
(previousValue, element) => previousValue.intersection(element), (previousValue, element) => previousValue.intersection(element),
) )
.toList(); .toList();
@@ -49,14 +51,10 @@ class _FullscreenBulkEditTagsWidgetState
.toSet() .toSet()
.difference(_sharedTags.toSet()) .difference(_sharedTags.toSet())
.toList(); .toList();
_filteredTags = state.tags.keys.toList(); _filteredTags = labels.tags.keys.toList();
_controller.addListener(() { _controller.addListener(() {
setState(() { setState(() {
_filteredTags = context _filteredTags = labels.tags.values
.read<DocumentBulkActionCubit>()
.state
.tags
.values
.where((e) => .where((e) =>
e.name.normalized().contains(_controller.text.normalized())) e.name.normalized().contains(_controller.text.normalized()))
.map((e) => e.id!) .map((e) => e.id!)
@@ -69,6 +67,7 @@ class _FullscreenBulkEditTagsWidgetState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labelRepository = context.watch<LabelRepository>();
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>( return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) { builder: (context, state) {
return FullscreenSelectionForm( return FullscreenSelectionForm(
@@ -86,7 +85,7 @@ class _FullscreenBulkEditTagsWidgetState
selectionBuilder: (context, index) { selectionBuilder: (context, index) {
return _buildTagOption( return _buildTagOption(
_filteredTags[index], _filteredTags[index],
state.tags, labelRepository.tags,
); );
}, },
selectionCount: _filteredTags.length, selectionCount: _filteredTags.length,
@@ -155,11 +154,12 @@ class _FullscreenBulkEditTagsWidgetState
void _submit() async { void _submit() async {
if (_addTags.isNotEmpty || _removeTags.isNotEmpty) { if (_addTags.isNotEmpty || _removeTags.isNotEmpty) {
final bloc = context.read<DocumentBulkActionCubit>(); final bloc = context.read<DocumentBulkActionCubit>();
final labelRepository = context.read<LabelRepository>();
final addNames = _addTags final addNames = _addTags
.map((value) => "\"${bloc.state.tags[value]!.name}\"") .map((value) => "\"${labelRepository.tags[value]!.name}\"")
.toList(); .toList();
final removeNames = _removeTags final removeNames = _removeTags
.map((value) => "\"${bloc.state.tags[value]!.name}\"") .map((value) => "\"${labelRepository.tags[value]!.name}\"")
.toList(); .toList();
final shouldPerformAction = await showDialog<bool>( final shouldPerformAction = await showDialog<bool>(
context: context, context: context,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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 { class DocumentPermissionsWidget extends StatefulWidget {
final DocumentModel document; final DocumentModel document;
@@ -13,8 +16,20 @@ class DocumentPermissionsWidget extends StatefulWidget {
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> { class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const SliverToBoxAdapter( return BlocBuilder<UserRepository, UserRepositoryState>(
child: Placeholder(), 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, Padding _buildEditForm(BuildContext context, DocumentEditState state,
FieldSuggestions? filteredSuggestions, UserModel currentUser) { FieldSuggestions? filteredSuggestions, UserModel currentUser) {
final labelRepository = context.watch<LabelRepository>();
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: TabBarView( child: TabBarView(
@@ -211,8 +213,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
).push<Correspondent>(context), ).push<Correspondent>(context),
addLabelText: S.of(context)!.addCorrespondent, addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent, labelText: S.of(context)!.correspondent,
options: options: labelRepository.correspondents,
context.watch<LabelRepository>().state.correspondents,
initialValue: state.document.correspondent != null initialValue: state.document.correspondent != null
? SetIdQueryParameter( ? SetIdQueryParameter(
id: state.document.correspondent!) id: state.document.correspondent!)
@@ -243,8 +244,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
? SetIdQueryParameter( ? SetIdQueryParameter(
id: state.document.documentType!) id: state.document.documentType!)
: const UnsetIdQueryParameter(), : const UnsetIdQueryParameter(),
options: options: labelRepository.documentTypes,
context.watch<LabelRepository>().state.documentTypes,
name: _DocumentEditPageState.fkDocumentType, name: _DocumentEditPageState.fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
@@ -266,8 +266,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
canCreateNewLabel: currentUser.canCreateStoragePaths, canCreateNewLabel: currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath, addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
options: options: labelRepository.storagePaths,
context.watch<LabelRepository>().state.storagePaths,
initialValue: state.document.storagePath != null initialValue: state.document.storagePath != null
? SetIdQueryParameter(id: state.document.storagePath!) ? SetIdQueryParameter(id: state.document.storagePath!)
: const UnsetIdQueryParameter(), : const UnsetIdQueryParameter(),
@@ -280,7 +279,7 @@ class _DocumentEditPageState extends State<DocumentEditPage>
// Tag form field // Tag form field
if (currentUser.canViewTags) if (currentUser.canViewTags)
TagsFormField( TagsFormField(
options: context.watch<LabelRepository>().state.tags, options: labelRepository.tags,
name: fkTags, name: fkTags,
allowOnlySelection: true, allowOnlySelection: true,
allowCreation: true, allowCreation: true,

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.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/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart'; import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
@@ -33,24 +34,31 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
DateTime? createdAt, DateTime? createdAt,
int? asn, int? asn,
}) async { }) async {
final taskId = await _documentApi.create( try {
bytes, final taskId = await _documentApi.create(
filename: filename, bytes,
title: title, filename: filename,
correspondent: correspondent, title: title,
documentType: documentType, correspondent: correspondent,
tags: tags, documentType: documentType,
createdAt: createdAt, tags: tags,
asn: asn, createdAt: createdAt,
onProgressChanged: (progress) { asn: asn,
if (!isClosed) { onProgressChanged: (progress) {
emit(state.copyWith(uploadProgress: progress)); if (!isClosed) {
} emit(state.copyWith(uploadProgress: progress));
}, }
); },
if (taskId != null) { );
_tasksNotifier.listenToTaskChanges(taskId); if (taskId != null) {
_tasksNotifier.listenToTaskChanges(taskId);
}
return taskId;
} on PaperlessApiException catch (error) {
addError(TransientPaperlessApiError(
code: error.code,
details: error.details,
));
} }
return taskId;
} }
} }

View File

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

View File

@@ -1,8 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hydrated_bloc/hydrated_bloc.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_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart'; import 'package:paperless_mobile/core/extensions/document_extensions.dart';
@@ -20,7 +18,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override @override
final ConnectivityStatusService connectivityStatusService; final ConnectivityStatusService connectivityStatusService;
@@ -32,7 +29,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
DocumentsCubit( DocumentsCubit(
this.api, this.api,
this.notifier, this.notifier,
this._labelRepository,
this._userState, this._userState,
this.connectivityStatusService, this.connectivityStatusService,
) : super(DocumentsState( ) : 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 { Future<void> bulkDelete(List<DocumentModel> documents) async {
@@ -111,7 +96,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
@override @override
Future<void> close() { Future<void> close() {
notifier.removeListener(this); notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }

View File

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

View File

@@ -489,10 +489,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
initialFilter: context.read<DocumentsCubit>().state.filter, initialFilter: context.read<DocumentsCubit>().state.filter,
scrollController: controller, scrollController: controller,
draggableSheetController: draggableSheetController, 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) { Widget build(BuildContext context) {
final subtitleStyle = final subtitleStyle =
Theme.of(context).textTheme.labelMedium?.apply(color: Colors.grey); Theme.of(context).textTheme.labelMedium?.apply(color: Colors.grey);
final labelRepository = context.watch<LabelRepository>();
return RichText( return RichText(
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -37,11 +38,8 @@ class DateAndDocumentTypeLabelWidget extends StatelessWidget {
? () => onDocumentTypeSelected!(document.documentType) ? () => onDocumentTypeSelected!(document.documentType)
: null, : null,
child: Text( child: Text(
context labelRepository
.watch<LabelRepository>() .documentTypes[document.documentType]!.name,
.state
.documentTypes[document.documentType]!
.name,
style: subtitleStyle, style: subtitleStyle,
), ),
), ),

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
import 'package:flutter/material.dart'; 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.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/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/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
@@ -32,7 +29,7 @@ class DocumentListItem extends DocumentItem {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state; final labelRepository = context.watch<LabelRepository>();
return ListTile( return ListTile(
tileColor: backgroundColor, tileColor: backgroundColor,
@@ -51,10 +48,8 @@ class DocumentListItem extends DocumentItem {
absorbing: isSelectionActive, absorbing: isSelectionActive,
child: CorrespondentWidget( child: CorrespondentWidget(
isClickable: isLabelClickable, isClickable: isLabelClickable,
correspondent: context correspondent:
.watch<LabelRepository>() labelRepository.correspondents[document.correspondent],
.state
.correspondents[document.correspondent],
onSelected: onCorrespondentSelected, onSelected: onCorrespondentSelected,
), ),
), ),
@@ -70,8 +65,8 @@ class DocumentListItem extends DocumentItem {
child: TagsWidget( child: TagsWidget(
isClickable: isLabelClickable, isClickable: isLabelClickable,
tags: document.tags tags: document.tags
.where((e) => labels.tags.containsKey(e)) .where((e) => labelRepository.tags.containsKey(e))
.map((e) => labels.tags[e]!) .map((e) => labelRepository.tags[e]!)
.toList(), .toList(),
onTagSelected: (id) => onTagSelected?.call(id), 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:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_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 DocumentFilter initialFilter;
final ScrollController? scrollController; final ScrollController? scrollController;
final EdgeInsets padding; 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({ const DocumentFilterForm({
super.key, super.key,
@@ -59,10 +56,6 @@ class DocumentFilterForm extends StatefulWidget {
required this.initialFilter, required this.initialFilter,
this.scrollController, this.scrollController,
this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16), this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
}); });
@override @override
@@ -80,13 +73,14 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labelRepository = context.watch<LabelRepository>();
return FormBuilder( return FormBuilder(
key: widget.formKey, key: widget.formKey,
child: CustomScrollView( child: CustomScrollView(
controller: widget.scrollController, controller: widget.scrollController,
slivers: [ slivers: [
if (widget.header != null) widget.header!, if (widget.header != null) widget.header!,
..._buildFormFieldList(), ..._buildFormFieldList(labelRepository),
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
height: 32, height: 32,
@@ -97,7 +91,7 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
); );
} }
List<Widget> _buildFormFieldList() { List<Widget> _buildFormFieldList(LabelRepository labelRepository) {
return [ return [
_buildQueryFormField(), _buildQueryFormField(),
Align( Align(
@@ -123,10 +117,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
_checkQueryConstraints(); _checkQueryConstraints();
}, },
), ),
_buildCorrespondentFormField(), _buildCorrespondentFormField(labelRepository.correspondents),
_buildDocumentTypeFormField(), _buildDocumentTypeFormField(labelRepository.documentTypes),
_buildStoragePathFormField(), _buildStoragePathFormField(labelRepository.storagePaths),
_buildTagsFormField(), _buildTagsFormField(labelRepository.tags),
] ]
.map((w) => SliverPadding( .map((w) => SliverPadding(
padding: widget.padding, padding: widget.padding,
@@ -151,10 +145,10 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
} }
} }
Widget _buildDocumentTypeFormField() { Widget _buildDocumentTypeFormField(Map<int, DocumentType> documentTypes) {
return LabelFormField<DocumentType>( return LabelFormField<DocumentType>(
name: DocumentFilterForm.fkDocumentType, name: DocumentFilterForm.fkDocumentType,
options: widget.documentTypes, options: documentTypes,
labelText: S.of(context)!.documentType, labelText: S.of(context)!.documentType,
initialValue: widget.initialFilter.documentType, initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined), 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>( return LabelFormField<Correspondent>(
name: DocumentFilterForm.fkCorrespondent, name: DocumentFilterForm.fkCorrespondent,
options: widget.correspondents, options: correspondents,
labelText: S.of(context)!.correspondent, labelText: S.of(context)!.correspondent,
initialValue: widget.initialFilter.correspondent, initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline), 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>( return LabelFormField<StoragePath>(
name: DocumentFilterForm.fkStoragePath, name: DocumentFilterForm.fkStoragePath,
options: widget.storagePaths, options: storagePaths,
labelText: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
initialValue: widget.initialFilter.storagePath, initialValue: widget.initialFilter.storagePath,
prefixIcon: const Icon(Icons.folder_outlined), 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( return TagsFormField(
name: DocumentModel.tagsKey, name: DocumentModel.tagsKey,
initialValue: widget.initialFilter.tags, initialValue: widget.initialFilter.tags,
options: widget.tags, options: tags,
allowExclude: false, allowExclude: false,
allowOnlySelection: false, allowOnlySelection: false,
allowCreation: false, allowCreation: false,

View File

@@ -13,20 +13,12 @@ class DocumentFilterPanel extends StatefulWidget {
final DocumentFilter initialFilter; final DocumentFilter initialFilter;
final ScrollController scrollController; final ScrollController scrollController;
final DraggableScrollableController draggableSheetController; 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({ const DocumentFilterPanel({
Key? key, Key? key,
required this.initialFilter, required this.initialFilter,
required this.scrollController, required this.scrollController,
required this.draggableSheetController, required this.draggableSheetController,
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -104,10 +96,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
scrollController: widget.scrollController, scrollController: widget.scrollController,
initialFilter: widget.initialFilter, initialFilter: widget.initialFilter,
header: _buildPanelHeader(), 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:paperless_api/paperless_api.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/translation/sort_field_localization_mapper.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.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 { class SortFieldSelectionBottomSheet extends StatefulWidget {
final SortOrder initialSortOrder; final SortOrder initialSortOrder;
final SortField? initialSortField; 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; final Future Function(SortField? field, SortOrder order) onSubmit;
@@ -20,10 +18,6 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
required this.initialSortOrder, required this.initialSortOrder,
required this.initialSortField, required this.initialSortField,
required this.onSubmit, required this.onSubmit,
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
}); });
@override @override
@@ -45,6 +39,7 @@ class _SortFieldSelectionBottomSheetState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labelRepository = context.watch<LabelRepository>();
return ClipRRect( return ClipRRect(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@@ -75,7 +70,7 @@ class _SortFieldSelectionBottomSheetState
_buildSortOption(SortField.archiveSerialNumber), _buildSortOption(SortField.archiveSerialNumber),
_buildSortOption( _buildSortOption(
SortField.correspondentName, SortField.correspondentName,
enabled: widget.correspondents.values.fold<bool>( enabled: labelRepository.correspondents.values.fold<bool>(
false, false,
(previousValue, element) => (previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0), previousValue || (element.documentCount ?? 0) > 0),
@@ -83,7 +78,7 @@ class _SortFieldSelectionBottomSheetState
_buildSortOption(SortField.title), _buildSortOption(SortField.title),
_buildSortOption( _buildSortOption(
SortField.documentType, SortField.documentType,
enabled: widget.documentTypes.values.fold<bool>( enabled: labelRepository.documentTypes.values.fold<bool>(
false, false,
(previousValue, element) => (previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0), 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:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.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/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/generated/l10n/app_localizations.dart';
class AddLabelPage<T extends Label> extends StatelessWidget { class AddLabelPage<T extends Label> extends StatelessWidget {
@@ -25,7 +25,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read<LabelRepository>(), context.read<LabelRepository>(),
), ),
child: AddLabelFormWidget( 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_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_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/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/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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
@@ -35,7 +35,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read<LabelRepository>(), context.read<LabelRepository>(),
), ),
child: EditLabelForm( child: EditLabelForm(

View File

@@ -1,8 +1,8 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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/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'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddCorrespondentPage extends StatelessWidget { class AddCorrespondentPage extends StatelessWidget {
@@ -12,7 +12,7 @@ class AddCorrespondentPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read(), context.read(),
), ),
child: AddLabelPage<Correspondent>( child: AddLabelPage<Correspondent>(
@@ -20,7 +20,7 @@ class AddCorrespondentPage extends StatelessWidget {
fromJsonT: Correspondent.fromJson, fromJsonT: Correspondent.fromJson,
initialName: initialName, initialName: initialName,
onSubmit: (context, label) => 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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/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'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddDocumentTypePage extends StatelessWidget { class AddDocumentTypePage extends StatelessWidget {
@@ -15,7 +15,7 @@ class AddDocumentTypePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read(), context.read(),
), ),
child: AddLabelPage<DocumentType>( child: AddLabelPage<DocumentType>(
@@ -23,7 +23,7 @@ class AddDocumentTypePage extends StatelessWidget {
fromJsonT: DocumentType.fromJson, fromJsonT: DocumentType.fromJson,
initialName: initialName, initialName: initialName,
onSubmit: (context, label) => 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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/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/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -13,7 +13,7 @@ class AddStoragePathPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read(), context.read(),
), ),
child: AddLabelPage<StoragePath>( child: AddLabelPage<StoragePath>(
@@ -21,7 +21,7 @@ class AddStoragePathPage extends StatelessWidget {
fromJsonT: StoragePath.fromJson, fromJsonT: StoragePath.fromJson,
initialName: initialName, initialName: initialName,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label), context.read<LabelCubit>().addStoragePath(label),
additionalFields: const [ additionalFields: const [
StoragePathAutofillFormBuilderField(name: StoragePath.pathKey), StoragePathAutofillFormBuilderField(name: StoragePath.pathKey),
SizedBox(height: 120.0), 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:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.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/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/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'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class AddTagPage extends StatelessWidget { class AddTagPage extends StatelessWidget {
@@ -16,15 +16,14 @@ class AddTagPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read(), context.read(),
), ),
child: AddLabelPage<Tag>( child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context)!.addTag), pageTitle: Text(S.of(context)!.addTag),
fromJsonT: Tag.fromJson, fromJsonT: Tag.fromJson,
initialName: initialName, initialName: initialName,
onSubmit: (context, label) => onSubmit: (context, label) => context.read<LabelCubit>().addTag(label),
context.read<EditLabelCubit>().addTag(label),
additionalFields: [ additionalFields: [
FormBuilderColorPickerField( FormBuilderColorPickerField(
name: Tag.colorKey, name: Tag.colorKey,

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
class EditStoragePathPage extends StatelessWidget { class EditStoragePathPage extends StatelessWidget {
@@ -13,16 +13,16 @@ class EditStoragePathPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read(), context.read(),
), ),
child: EditLabelPage<StoragePath>( child: EditLabelPage<StoragePath>(
label: storagePath, label: storagePath,
fromJsonT: StoragePath.fromJson, fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceStoragePath(label), context.read<LabelCubit>().replaceStoragePath(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label), context.read<LabelCubit>().removeStoragePath(label),
canDelete: context canDelete: context
.watch<LocalUserAccount>() .watch<LocalUserAccount>()
.paperlessUser .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_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/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'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class EditTagPage extends StatelessWidget { class EditTagPage extends StatelessWidget {
@@ -16,16 +16,16 @@ class EditTagPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => LabelCubit(
context.read(), context.read(),
), ),
child: EditLabelPage<Tag>( child: EditLabelPage<Tag>(
label: tag, label: tag,
fromJsonT: Tag.fromJson, fromJsonT: Tag.fromJson,
onSubmit: (context, label) => onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceTag(label), context.read<LabelCubit>().replaceTag(label),
onDelete: (context, label) => onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label), context.read<LabelCubit>().removeTag(label),
canDelete: canDelete:
context.watch<LocalUserAccount>().paperlessUser.canDeleteTags, context.watch<LocalUserAccount>().paperlessUser.canDeleteTags,
additionalFields: [ additionalFields: [

View File

@@ -53,8 +53,8 @@ class HomeShellWidget extends StatelessWidget {
builder: (context, box, _) { builder: (context, box, _) {
if (currentUserId == null) { if (currentUserId == null) {
//This only happens during logout... //This only happens during logout...
//TODO: Find way so this does not occur anymore //FIXME: Find way so this does not occur anymore
return SizedBox.shrink(); return const SizedBox.shrink();
} }
final currentLocalUser = box.get(currentUserId)!; final currentLocalUser = box.get(currentUserId)!;
return MultiProvider( return MultiProvider(
@@ -107,36 +107,31 @@ class HomeShellWidget extends StatelessWidget {
), ),
if (currentLocalUser.hasMultiUserSupport) if (currentLocalUser.hasMultiUserSupport)
Provider( Provider(
create: (context) => PaperlessUserApiV3Impl( create: (context) => paperlessProviderFactory.createUserApi(
context.read<SessionManager>().client, context.read<SessionManager>().client,
apiVersion: paperlessApiVersion,
), ),
), ),
], ],
builder: (context, _) { builder: (context, _) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
Provider( ChangeNotifierProvider(
create: (context) { create: (context) {
final repo = LabelRepository(context.read()); return LabelRepository(context.read())
if (currentLocalUser ..initialize(
.paperlessUser.canViewCorrespondents) { loadCorrespondents: currentLocalUser
repo.findAllCorrespondents(); .paperlessUser.canViewCorrespondents,
} loadDocumentTypes: currentLocalUser
if (currentLocalUser .paperlessUser.canViewDocumentTypes,
.paperlessUser.canViewDocumentTypes) { loadStoragePaths: currentLocalUser
repo.findAllDocumentTypes(); .paperlessUser.canViewStoragePaths,
} loadTags:
if (currentLocalUser.paperlessUser.canViewTags) { currentLocalUser.paperlessUser.canViewTags,
repo.findAllTags(); );
}
if (currentLocalUser
.paperlessUser.canViewStoragePaths) {
repo.findAllStoragePaths();
}
return repo;
}, },
), ),
Provider( ChangeNotifierProvider(
create: (context) { create: (context) {
final repo = SavedViewRepository(context.read()); final repo = SavedViewRepository(context.read());
if (currentLocalUser.paperlessUser.canViewSavedViews) { if (currentLocalUser.paperlessUser.canViewSavedViews) {
@@ -145,6 +140,12 @@ class HomeShellWidget extends StatelessWidget {
return repo; return repo;
}, },
), ),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => UserRepository(
context.read(),
)..initialize(),
),
], ],
builder: (context, _) { builder: (context, _) {
return MultiProvider( return MultiProvider(
@@ -152,7 +153,6 @@ class HomeShellWidget extends StatelessWidget {
Provider( Provider(
lazy: false, lazy: false,
create: (context) => DocumentsCubit( create: (context) => DocumentsCubit(
context.read(),
context.read(), context.read(),
context.read(), context.read(),
Hive.box<LocalUserAppState>( Hive.box<LocalUserAppState>(
@@ -196,12 +196,6 @@ class HomeShellWidget extends StatelessWidget {
context.read(), context.read(),
), ),
), ),
if (currentLocalUser.hasMultiUserSupport)
Provider(
create: (context) => UserRepository(
context.read(),
)..initialize(),
),
], ],
child: child, 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/features/logging/data/logger.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.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.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/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/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.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._labelRepository,
this.notifier, this.notifier,
this.connectivityStatusService, this.connectivityStatusService,
) : super(InboxState(labels: _labelRepository.state)) { ) : super(const InboxState()) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
@@ -62,12 +61,6 @@ class InboxCubit extends HydratedCubit<InboxState>
} }
}, },
); );
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(state.copyWith(labels: labels));
},
);
} }
@override @override
@@ -112,7 +105,7 @@ class InboxCubit extends HydratedCubit<InboxState>
if (inboxTags.isEmpty) { if (inboxTags.isEmpty) {
// no inbox tags = no inbox items. // no inbox tags = no inbox items.
return emit( return emit(
state.copyWith( state.copyWith(
hasLoaded: true, hasLoaded: true,
value: [], value: [],
@@ -256,7 +249,6 @@ class InboxCubit extends HydratedCubit<InboxState>
@override @override
Future<void> close() { Future<void> close() {
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }

View File

@@ -4,8 +4,6 @@ part of 'inbox_cubit.dart';
class InboxState extends DocumentPagingState { class InboxState extends DocumentPagingState {
final Iterable<int> inboxTags; final Iterable<int> inboxTags;
final LabelRepositoryState labels;
final int itemsInInboxCount; final int itemsInInboxCount;
@JsonKey() @JsonKey()
@@ -19,7 +17,6 @@ class InboxState extends DocumentPagingState {
this.inboxTags = const [], this.inboxTags = const [],
this.isHintAcknowledged = false, this.isHintAcknowledged = false,
this.itemsInInboxCount = 0, this.itemsInInboxCount = 0,
this.labels = const LabelRepositoryState(),
}); });
@override @override
@@ -32,7 +29,6 @@ class InboxState extends DocumentPagingState {
documents, documents,
isHintAcknowledged, isHintAcknowledged,
itemsInInboxCount, itemsInInboxCount,
labels,
]; ];
InboxState copyWith({ InboxState copyWith({
@@ -42,7 +38,6 @@ class InboxState extends DocumentPagingState {
List<PagedSearchResult<DocumentModel>>? value, List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter, DocumentFilter? filter,
bool? isHintAcknowledged, bool? isHintAcknowledged,
LabelRepositoryState? labels,
Map<int, FieldSuggestions>? suggestions, Map<int, FieldSuggestions>? suggestions,
int? itemsInInboxCount, int? itemsInInboxCount,
}) { }) {
@@ -52,7 +47,6 @@ class InboxState extends DocumentPagingState {
value: value ?? super.value, value: value ?? super.value,
inboxTags: inboxTags ?? this.inboxTags, inboxTags: inboxTags ?? this.inboxTags,
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged, isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
labels: labels ?? this.labels,
filter: filter ?? super.filter, filter: filter ?? super.filter,
itemsInInboxCount: itemsInInboxCount ?? this.itemsInInboxCount, 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
@@ -148,6 +150,7 @@ class _InboxItemState extends State<InboxItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labelRepository = context.read<LabelRepository>();
return BlocBuilder<InboxCubit, InboxState>( return BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) { builder: (context, state) {
return GestureDetector( return GestureDetector(
@@ -193,7 +196,7 @@ class _InboxItemState extends State<InboxItem> {
?.fontSize, ?.fontSize,
), ),
LabelText<Correspondent>( LabelText<Correspondent>(
label: state.labels.correspondents[ label: labelRepository.correspondents[
widget.document.correspondent], widget.document.correspondent],
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-", placeholder: "-",
@@ -208,7 +211,7 @@ class _InboxItemState extends State<InboxItem> {
?.fontSize, ?.fontSize,
), ),
LabelText<DocumentType>( LabelText<DocumentType>(
label: state.labels.documentTypes[ label: labelRepository.documentTypes[
widget.document.documentType], widget.document.documentType],
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-", placeholder: "-",
@@ -217,8 +220,8 @@ class _InboxItemState extends State<InboxItem> {
const Spacer(), const Spacer(),
TagsWidget( TagsWidget(
tags: widget.document.tags tags: widget.document.tags
.map((e) => state.labels.tags[e]) .map((e) => labelRepository.tags[e])
.whereNot((e) => e == null) .where(isNotNull)
.toList() .toList()
.cast<Tag>(), .cast<Tag>(),
isClickable: false, isClickable: false,

View File

@@ -2,41 +2,134 @@ import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.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_cubit.freezed.dart';
part 'label_state.dart'; part 'label_state.dart';
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> { class LabelCubit extends Cubit<LabelState> {
@override
final LabelRepository labelRepository; final LabelRepository labelRepository;
LabelCubit(this.labelRepository) : super(const LabelState()) { LabelCubit(this.labelRepository) : super(const LabelState()) {
labelRepository.addListener( labelRepository.addListener(
this, () {
onChanged: (labels) {
emit(state.copyWith( emit(state.copyWith(
correspondents: labels.correspondents, correspondents: labelRepository.correspondents,
documentTypes: labels.documentTypes, documentTypes: labelRepository.documentTypes,
storagePaths: labels.storagePaths, storagePaths: labelRepository.storagePaths,
tags: labels.tags, tags: labelRepository.tags,
)); ));
}, },
); );
} }
Future<void> reload() { Future<void> reload({
return Future.wait([ required bool loadCorrespondents,
labelRepository.findAllCorrespondents(), required bool loadDocumentTypes,
labelRepository.findAllDocumentTypes(), required bool loadStoragePaths,
labelRepository.findAllTags(), required bool loadTags,
labelRepository.findAllStoragePaths(), }) {
]); 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 @override
Future<void> close() { Future<void> close() {
labelRepository.removeListener(this);
return super.close(); 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; final ConnectivityStatusService connectivityStatusService;
@override @override
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
final LabelRepository _labelRepository;
LinkedDocumentsCubit( LinkedDocumentsCubit(
DocumentFilter filter, DocumentFilter filter,
this.api, this.api,
this.notifier, this.notifier,
this._labelRepository,
this.connectivityStatusService, this.connectivityStatusService,
) : super(LinkedDocumentsState(filter: filter)) { ) : super(LinkedDocumentsState(filter: filter)) {
updateFilter(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( notifier.addListener(
this, this,
onUpdated: replace, onUpdated: replace,

View File

@@ -5,21 +5,12 @@ class LinkedDocumentsState extends DocumentPagingState {
@JsonKey() @JsonKey()
final ViewType viewType; 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({ const LinkedDocumentsState({
this.viewType = ViewType.list, this.viewType = ViewType.list,
super.filter = const DocumentFilter(), super.filter = const DocumentFilter(),
super.isLoading, super.isLoading,
super.hasLoaded, super.hasLoaded,
super.value, super.value,
this.correspondents = const {},
this.documentTypes = const {},
this.storagePaths = const {},
this.tags = const {},
}); });
LinkedDocumentsState copyWith({ LinkedDocumentsState copyWith({
@@ -39,10 +30,6 @@ class LinkedDocumentsState extends DocumentPagingState {
hasLoaded: hasLoaded ?? this.hasLoaded, hasLoaded: hasLoaded ?? this.hasLoaded,
value: value ?? this.value, value: value ?? this.value,
viewType: viewType ?? this.viewType, 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 @override
List<Object?> get props => [ List<Object?> get props => [
viewType, viewType,
correspondents,
documentTypes,
tags,
storagePaths,
...super.props, ...super.props,
]; ];

View File

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

View File

@@ -1,9 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.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/exception/server_message_exception.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.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/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.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/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/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routing/routes/app_logs_route.dart';
class AddAccountPage extends StatefulWidget { class AddAccountPage extends StatefulWidget {
final FutureOr<void> Function( final FutureOr<void> Function(
@@ -58,10 +65,172 @@ class _AddAccountPageState extends State<AddAccountPage> {
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
bool _isCheckingConnection = false; bool _isCheckingConnection = false;
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown; ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
bool _isFormSubmitted = false; bool _isFormSubmitted = false;
final _pageController = PageController();
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.titleText), title: Text(widget.titleText),
@@ -91,7 +260,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
children: [ children: [
ServerAddressFormField( ServerAddressFormField(
initialValue: widget.initialServerUrl, initialValue: widget.initialServerUrl,
onSubmit: (address) { onChanged: (address) {
_updateReachability(address); _updateReachability(address);
}, },
).padded(), ).padded(),
@@ -117,7 +286,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
.withOpacity(0.6), .withOpacity(0.6),
), ),
).padded(16), ).padded(16),
] ],
], ],
), ),
), ),
@@ -125,7 +294,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
); );
} }
Future<void> _updateReachability([String? address]) async { Future<ReachabilityStatus> _updateReachability([String? address]) async {
setState(() { setState(() {
_isCheckingConnection = true; _isCheckingConnection = true;
}); });
@@ -150,13 +319,10 @@ class _AddAccountPageState extends State<AddAccountPage> {
_isCheckingConnection = false; _isCheckingConnection = false;
_reachabilityStatus = status; _reachabilityStatus = status;
}); });
return status;
} }
Widget _buildStatusIndicator() { Widget _buildStatusIndicator() {
if (_isCheckingConnection) {
return const ListTile();
}
Widget _buildIconText( Widget _buildIconText(
IconData icon, IconData icon,
String text, [ String text, [
@@ -176,14 +342,6 @@ class _AddAccountPageState extends State<AddAccountPage> {
Color errorColor = Theme.of(context).colorScheme.error; Color errorColor = Theme.of(context).colorScheme.error;
switch (_reachabilityStatus) { switch (_reachabilityStatus) {
case ReachabilityStatus.unknown:
return Container();
case ReachabilityStatus.reachable:
return _buildIconText(
Icons.done,
S.of(context)!.connectionSuccessfulylEstablished,
Colors.green,
);
case ReachabilityStatus.notReachable: case ReachabilityStatus.notReachable:
return _buildIconText( return _buildIconText(
Icons.close, Icons.close,
@@ -214,6 +372,8 @@ class _AddAccountPageState extends State<AddAccountPage> {
S.of(context)!.connectionTimedOut, S.of(context)!.connectionTimedOut,
errorColor, 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/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.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/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'; import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget { class ClientCertificateFormField extends StatefulWidget {
@@ -16,10 +17,10 @@ class ClientCertificateFormField extends StatefulWidget {
final String? initialPassphrase; final String? initialPassphrase;
final Uint8List? initialBytes; final Uint8List? initialBytes;
final void Function(ClientCertificateFormModel? cert) onChanged; final ValueChanged<ClientCertificateFormModel?>? onChanged;
const ClientCertificateFormField({ const ClientCertificateFormField({
super.key, super.key,
required this.onChanged, this.onChanged,
this.initialPassphrase, this.initialPassphrase,
this.initialBytes, this.initialBytes,
}); });
@@ -29,13 +30,15 @@ class ClientCertificateFormField extends StatefulWidget {
_ClientCertificateFormFieldState(); _ClientCertificateFormFieldState();
} }
class _ClientCertificateFormFieldState class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
extends State<ClientCertificateFormField> { with AutomaticKeepAliveClientMixin {
File? _selectedFile; File? _selectedFile;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<ClientCertificateFormModel?>( return FormBuilderField<ClientCertificateFormModel?>(
key: const ValueKey('login-client-cert'), key: const ValueKey('login-client-cert'),
name: ClientCertificateFormField.fkClientCertificate,
onChanged: widget.onChanged, onChanged: widget.onChanged,
initialValue: widget.initialBytes != null initialValue: widget.initialBytes != null
? ClientCertificateFormModel( ? ClientCertificateFormModel(
@@ -43,16 +46,6 @@ class _ClientCertificateFormFieldState
passphrase: widget.initialPassphrase, passphrase: widget.initialPassphrase,
) )
: null, : 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) { builder: (field) {
final theme = final theme =
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new 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) { if (result == null || result.files.single.path == null) {
return; 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!); File file = File(result.files.single.path!);
setState(() { setState(() {
_selectedFile = file; _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 { class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress"; static const String fkServerAddress = "serverAddress";
final String? initialValue; final String? initialValue;
final void Function(String? address) onSubmit; final ValueChanged<String?>? onChanged;
const ServerAddressFormField({ const ServerAddressFormField({
Key? key, Key? key,
required this.onSubmit, this.onChanged,
this.initialValue, this.initialValue,
}) : super(key: key); }) : super(key: key);
@@ -20,8 +21,10 @@ class ServerAddressFormField extends StatefulWidget {
State<ServerAddressFormField> createState() => _ServerAddressFormFieldState(); State<ServerAddressFormField> createState() => _ServerAddressFormFieldState();
} }
class _ServerAddressFormFieldState extends State<ServerAddressFormField> { class _ServerAddressFormFieldState extends State<ServerAddressFormField>
with AutomaticKeepAliveClientMixin {
bool _canClear = false; bool _canClear = false;
final _textFieldKey = GlobalKey();
@override @override
void initState() { void initState() {
@@ -38,10 +41,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<String>( return FormBuilderField<String>(
initialValue: widget.initialValue, initialValue: widget.initialValue,
name: ServerAddressFormField.fkServerAddress, name: ServerAddressFormField.fkServerAddress,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: widget.onChanged,
builder: (field) { builder: (field) {
return RawAutocomplete<String>( return RawAutocomplete<String>(
focusNode: _focusNode, focusNode: _focusNode,
@@ -51,6 +56,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
onSelected: onSelected, onSelected: onSelected,
options: options, options: options,
maxOptionsHeight: 200.0, maxOptionsHeight: 200.0,
maxWidth: MediaQuery.sizeOf(context).width - 40,
); );
}, },
key: const ValueKey('login-server-address'), key: const ValueKey('login-server-address'),
@@ -60,12 +66,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.where((element) => element.contains(textEditingValue.text)); .where((element) => element.contains(textEditingValue.text));
}, },
onSelected: (option) { onSelected: (option) {
_formatInput(); _formatInput(field);
field.didChange(_textEditingController.text);
}, },
fieldViewBuilder: fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) { (context, textEditingController, focusNode, onFieldSubmitted) {
return TextFormField( return TextFormField(
key: _textFieldKey,
controller: textEditingController, controller: textEditingController,
focusNode: focusNode, focusNode: focusNode,
decoration: InputDecoration( decoration: InputDecoration(
@@ -78,15 +84,22 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
onPressed: () { onPressed: () {
textEditingController.clear(); textEditingController.clear();
field.didChange(textEditingController.text); field.didChange(textEditingController.text);
widget.onSubmit(textEditingController.text);
}, },
) )
: null, : null,
), ),
autofocus: false, autofocus: false,
onFieldSubmitted: (_) { onFieldSubmitted: (_) {
_formatInput(field);
onFieldSubmitted(); onFieldSubmitted();
_formatInput(); },
onTapOutside: (event) {
if (!FocusScope.of(context).hasFocus) {
return;
}
_formatInput(field);
onFieldSubmitted();
FocusScope.of(context).unfocus();
}, },
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) { validator: (value) {
@@ -113,7 +126,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
); );
} }
void _formatInput() { void _formatInput(FormFieldState<String> field) {
String address = _textEditingController.text.trim(); String address = _textEditingController.text.trim();
address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
_textEditingController.text = address; _textEditingController.text = address;
@@ -121,8 +134,11 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
baseOffset: address.length, baseOffset: address.length,
extentOffset: address.length, extentOffset: address.length,
); );
widget.onSubmit(address); field.didChange(_textEditingController.text);
} }
@override
bool get wantKeepAlive => true;
} }
/// Taken from [Autocomplete] /// Taken from [Autocomplete]
@@ -131,12 +147,14 @@ class _AutocompleteOptions extends StatelessWidget {
required this.onSelected, required this.onSelected,
required this.options, required this.options,
required this.maxOptionsHeight, required this.maxOptionsHeight,
required this.maxWidth,
}); });
final AutocompleteOnSelected<String> onSelected; final AutocompleteOnSelected<String> onSelected;
final Iterable<String> options; final Iterable<String> options;
final double maxOptionsHeight; final double maxOptionsHeight;
final double maxWidth;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -145,7 +163,10 @@ class _AutocompleteOptions extends StatelessWidget {
child: Material( child: Material(
elevation: 4.0, elevation: 4.0,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight), constraints: BoxConstraints(
maxHeight: maxOptionsHeight,
maxWidth: maxWidth,
),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
shrinkWrap: true, shrinkWrap: true,

View File

@@ -12,13 +12,13 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class UserCredentialsFormField extends StatefulWidget { class UserCredentialsFormField extends StatefulWidget {
static const fkCredentials = 'credentials'; static const fkCredentials = 'credentials';
final void Function() onFieldsSubmitted; final VoidCallback? onFieldsSubmitted;
final String? initialUsername; final String? initialUsername;
final String? initialPassword; final String? initialPassword;
final GlobalKey<FormBuilderState> formKey; final GlobalKey<FormBuilderState> formKey;
const UserCredentialsFormField({ const UserCredentialsFormField({
Key? key, Key? key,
required this.onFieldsSubmitted, this.onFieldsSubmitted,
this.initialUsername, this.initialUsername,
this.initialPassword, this.initialPassword,
required this.formKey, required this.formKey,
@@ -29,12 +29,14 @@ class UserCredentialsFormField extends StatefulWidget {
_UserCredentialsFormFieldState(); _UserCredentialsFormFieldState();
} }
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> { class _UserCredentialsFormFieldState extends State<UserCredentialsFormField>
with AutomaticKeepAliveClientMixin {
final _usernameFocusNode = FocusNode(); final _usernameFocusNode = FocusNode();
final _passwordFocusNode = FocusNode(); final _passwordFocusNode = FocusNode();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FormBuilderField<LoginFormCredentials?>( return FormBuilderField<LoginFormCredentials?>(
initialValue: LoginFormCredentials( initialValue: LoginFormCredentials(
password: widget.initialPassword, password: widget.initialPassword,
@@ -87,7 +89,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
LoginFormCredentials(password: password), LoginFormCredentials(password: password),
), ),
onFieldSubmitted: (_) { onFieldSubmitted: (_) {
widget.onFieldsSubmitted(); widget.onFieldsSubmitted?.call();
}, },
validator: (value) { validator: (value) {
if (value?.trim().isEmpty ?? true) { 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.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'; import 'package:paperless_mobile/theme.dart';
class LoginTransitionPage extends StatelessWidget { class LoginTransitionPage extends StatelessWidget {
@@ -20,10 +22,25 @@ class LoginTransitionPage extends StatelessWidget {
body: Stack( body: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
const CircularProgressIndicator(), Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Align(
alignment: Alignment.bottomCenter,
child: Text(text).paddedOnly(bottom: 24),
),
],
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Text(text).paddedOnly(bottom: 24), child: TextButton(
child: Text(S.of(context)!.appLogs('')),
onPressed: () {
AppLogsRoute().push(context);
},
),
), ),
], ],
).padded(16), ).padded(16),

View File

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

View File

@@ -34,32 +34,13 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
required this.savedView, required this.savedView,
int initialCount = 25, int initialCount = 25,
}) : super( }) : super(
SavedViewDetailsState( SavedViewDetailsState(viewType: _userState.savedViewsViewType),
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
tags: _labelRepository.state.tags,
storagePaths: _labelRepository.state.storagePaths,
viewType: _userState.savedViewsViewType,
),
) { ) {
notifier.addListener( notifier.addListener(
this, this,
onDeleted: remove, onDeleted: remove,
onUpdated: replace, 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( updateFilter(
filter: savedView.toDocumentFilter().copyWith( filter: savedView.toDocumentFilter().copyWith(
page: 1, 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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.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/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/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.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/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
@@ -17,15 +18,12 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override @override
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
SimilarDocumentsCubit( SimilarDocumentsCubit(
this.api, this.api,
this.notifier, this.notifier,
this._labelRepository,
this.connectivityStatusService, { this.connectivityStatusService, {
required this.documentId, required this.documentId,
}) : super(const SimilarDocumentsState(filter: DocumentFilter())) { }) : super(const SimilarDocumentsState(filter: DocumentFilter())) {
@@ -39,19 +37,30 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
@override @override
Future<void> initialize() async { Future<void> initialize() async {
if (!state.hasLoaded) { if (!state.hasLoaded) {
await updateFilter( try {
filter: state.filter.copyWith( await updateFilter(
moreLike: () => documentId, filter: state.filter.copyWith(
sortField: SortField.score, 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 @override
Future<void> close() { Future<void> close() {
notifier.removeListener(this); notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }

View File

@@ -1,19 +1,22 @@
part of 'similar_documents_cubit.dart'; part of 'similar_documents_cubit.dart';
class SimilarDocumentsState extends DocumentPagingState { class SimilarDocumentsState extends DocumentPagingState {
final ErrorCode? error;
const SimilarDocumentsState({ const SimilarDocumentsState({
required super.filter, required super.filter,
super.hasLoaded, super.hasLoaded,
super.isLoading, super.isLoading,
super.value, super.value,
this.error,
}); });
@override @override
List<Object> get props => [ List<Object?> get props => [
filter, filter,
hasLoaded, hasLoaded,
isLoading, isLoading,
value, value,
error,
]; ];
@override @override
@@ -36,12 +39,14 @@ class SimilarDocumentsState extends DocumentPagingState {
bool? isLoading, bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value, List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter, DocumentFilter? filter,
ErrorCode? error,
}) { }) {
return SimilarDocumentsState( return SimilarDocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded, hasLoaded: hasLoaded ?? this.hasLoaded,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
value: value ?? this.value, value: value ?? this.value,
filter: filter ?? this.filter, 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.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/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/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.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'; 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(), child: OfflineWidget(),
); );
} }
if (state.error != null) {
return SliverFillRemaining(
child: Center(
child: Text(
translateError(context, state.error!),
textAlign: TextAlign.center,
),
).padded(),
);
}
if (state.hasLoaded && if (state.hasLoaded &&
!state.isLoading && !state.isLoading &&
state.documents.isEmpty) { state.documents.isEmpty) {

View File

@@ -1010,14 +1010,19 @@
"couldNotLoadLogfileFrom": "No es pot carregar log desde {date}.", "couldNotLoadLogfileFrom": "No es pot carregar log desde {date}.",
"loadingLogsFrom": "Carregant registres des de {date}...", "loadingLogsFrom": "Carregant registres des de {date}...",
"clearLogs": "Netejar registres des de {date}", "clearLogs": "Netejar registres des de {date}",
"showPdf": "Show PDF", "showPdf": "Mostra PDF",
"@showPdf": { "@showPdf": {
"description": "Tooltip shown on the \"show pdf\" button on the document edit page" "description": "Tooltip shown on the \"show pdf\" button on the document edit page"
}, },
"hidePdf": "Hide PDF", "hidePdf": "Oculta PDF",
"@hidePdf": { "@hidePdf": {
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "misc": "Miscel·lanni",
"loggingOut": "Logging out..." "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Sonstige", "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "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": { "@donate": {
"description": "Label of the in-app donate button" "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": { "@donationDialogContent": {
"description": "Text displayed in the donation dialog" "description": "Text displayed in the donation dialog"
}, },
@@ -881,11 +881,11 @@
"@noDocumentsFound": { "@noDocumentsFound": {
"description": "Message shown when no documents were found." "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": { "@couldNotDeleteCorrespondent": {
"description": "Message shown in snackbar when a correspondent could not be deleted." "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": { "@couldNotDeleteDocumentType": {
"description": "Message shown when a document type could not be deleted" "description": "Message shown when a document type could not be deleted"
}, },
@@ -893,7 +893,7 @@
"@couldNotDeleteTag": { "@couldNotDeleteTag": {
"description": "Message shown when a tag could not be deleted" "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": { "@couldNotDeleteStoragePath": {
"description": "Message shown when a storage path could not be deleted" "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" "description": "Message shown when a saved view could not be updated"
}, },
"couldNotUpdateStoragePath": "No se pudo actualizar la ruta de almacenamiento, intente nuevamente.", "couldNotUpdateStoragePath": "No se pudo actualizar la ruta de almacenamiento, intente nuevamente.",
"savedViewSuccessfullyUpdated": "La vista guardada se actualizó correctamente.", "savedViewSuccessfullyUpdated": "Vista guardada actualizada correctamente.",
"@savedViewSuccessfullyUpdated": { "@savedViewSuccessfullyUpdated": {
"description": "Message shown when a saved view was successfully updated." "description": "Message shown when a saved view was successfully updated."
}, },
@@ -984,7 +984,7 @@
"@authenticatingDots": { "@authenticatingDots": {
"description": "Message shown when the app is authenticating the user" "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": "Obteniendo información del usuario...",
"@fetchingUserInformation": { "@fetchingUserInformation": {
"description": "Message shown when the app loads user data from the server" "description": "Message shown when the app loads user data from the server"
@@ -1001,7 +1001,7 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}.", "noLogsFoundOn": "No se encontraron registros en {date}.",
"logfileBottomReached": "Has alcanzado el final del archivo de registro.", "logfileBottomReached": "Has alcanzado el final del archivo de registro.",
"appLogs": "Registros de la aplicación {date}", "appLogs": "Registros de la aplicación {date}",
@@ -1010,14 +1010,19 @@
"couldNotLoadLogfileFrom": "No se pudo cargar el archivo de registro desde {date}.", "couldNotLoadLogfileFrom": "No se pudo cargar el archivo de registro desde {date}.",
"loadingLogsFrom": "Cargando registros desde {date}...", "loadingLogsFrom": "Cargando registros desde {date}...",
"clearLogs": "Limpiar registros desde {date}", "clearLogs": "Limpiar registros desde {date}",
"showPdf": "Show PDF", "showPdf": "Mostrar PDF",
"@showPdf": { "@showPdf": {
"description": "Tooltip shown on the \"show pdf\" button on the document edit page" "description": "Tooltip shown on the \"show pdf\" button on the document edit page"
}, },
"hidePdf": "Hide PDF", "hidePdf": "Ocultar PDF",
"@hidePdf": { "@hidePdf": {
"description": "Tooltip shown on the \"show pdf\" icon button on the document edit page" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "misc": "Otros",
"loggingOut": "Logging out..." "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Sonstige", "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "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" "description": "Tooltip shown on the \"show pdf\" icon button on the document edit page"
}, },
"misc": "Miscellaneous", "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/accessibility/accessible_page.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/my_bloc_observer.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.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/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
@@ -123,6 +124,7 @@ Future<void> _initHive() async {
void main() async { void main() async {
runZonedGuarded(() async { runZonedGuarded(() async {
Bloc.observer = MyBlocObserver();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await FileService.instance.initialize(); await FileService.instance.initialize();
@@ -371,6 +373,16 @@ class _GoRouterShellState extends State<GoRouterShell> {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
return MaterialApp.router( return MaterialApp.router(
builder: (context, child) {
return AnnotatedRegion<SystemUiOverlayStyle>(
child: child!,
value: buildOverlayStyle(
Theme.of(context),
systemNavigationBarColor:
Theme.of(context).colorScheme.background,
),
);
},
routerConfig: _router, routerConfig: _router,
debugShowCheckedModeBanner: true, debugShowCheckedModeBanner: true,
title: "Paperless Mobile", title: "Paperless Mobile",

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:paperless_api/paperless_api.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/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_label_page.dart';
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
@@ -37,7 +38,7 @@ class DocumentDetailsRoute extends GoRouteData {
final String? queryString; final String? queryString;
final String? thumbnailUrl; final String? thumbnailUrl;
final String? title; final String? title;
const DocumentDetailsRoute({ const DocumentDetailsRoute({
required this.id, required this.id,
this.isLabelClickable = true, this.isLabelClickable = true,
@@ -53,7 +54,6 @@ class DocumentDetailsRoute extends GoRouteData {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
context.read(),
id: id, id: id,
)..initialize(), )..initialize(),
lazy: false, lazy: false,
@@ -131,9 +131,9 @@ class BulkEditDocumentsRoute extends GoRouteData {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
final labelRepository = context.read<LabelRepository>();
return BlocProvider( return BlocProvider(
create: (_) => DocumentBulkActionCubit( create: (_) => DocumentBulkActionCubit(
context.read(),
context.read(), context.read(),
context.read(), context.read(),
selection: $extra.selection, selection: $extra.selection,
@@ -144,9 +144,9 @@ class BulkEditDocumentsRoute extends GoRouteData {
LabelType.tag => const FullscreenBulkEditTagsWidget(), LabelType.tag => const FullscreenBulkEditTagsWidget(),
_ => FullscreenBulkEditLabelPage( _ => FullscreenBulkEditLabelPage(
options: switch ($extra.type) { options: switch ($extra.type) {
LabelType.correspondent => state.correspondents, LabelType.correspondent => labelRepository.correspondents,
LabelType.documentType => state.documentTypes, LabelType.documentType => labelRepository.documentTypes,
LabelType.storagePath => state.storagePaths, LabelType.storagePath => labelRepository.storagePaths,
_ => throw Exception("Parameter not allowed here."), _ => throw Exception("Parameter not allowed here."),
}, },
selection: state.selection, 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/cubit/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart'; import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart';
import 'package:paperless_mobile/routing/navigation_keys.dart'; import 'package:paperless_mobile/routing/navigation_keys.dart';
class LabelsBranch extends StatefulShellBranchData { class LabelsBranch extends StatefulShellBranchData {
static final GlobalKey<NavigatorState> $navigatorKey = labelsNavigatorKey; static final GlobalKey<NavigatorState> $navigatorKey = labelsNavigatorKey;
const LabelsBranch(); const LabelsBranch();
@@ -81,7 +82,6 @@ class LinkedDocumentsRoute extends GoRouteData {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
context.read(),
), ),
child: const LinkedDocumentsPage(), 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:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.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/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'; import 'package:paperless_api/src/models/search_hit.dart';
part 'document_model.g.dart'; part 'document_model.g.dart';
@@ -50,6 +51,7 @@ class DocumentModel extends Equatable {
// Only present if full_perms=true // Only present if full_perms=true
final Permissions? permissions; final Permissions? permissions;
final Iterable<CustomFieldModel>? customFields;
const DocumentModel({ const DocumentModel({
required this.id, required this.id,
@@ -69,6 +71,7 @@ class DocumentModel extends Equatable {
this.owner, this.owner,
this.userCanChange, this.userCanChange,
this.permissions, this.permissions,
this.customFields,
}); });
factory DocumentModel.fromJson(Map<String, dynamic> json) => factory DocumentModel.fromJson(Map<String, dynamic> json) =>
@@ -89,6 +92,8 @@ class DocumentModel extends Equatable {
int? Function()? archiveSerialNumber, int? Function()? archiveSerialNumber,
String? originalFileName, String? originalFileName,
String? archivedFileName, String? archivedFileName,
int? Function()? owner,
bool? userCanChange,
}) { }) {
return DocumentModel( return DocumentModel(
id: id, id: id,
@@ -107,6 +112,8 @@ class DocumentModel extends Equatable {
? archiveSerialNumber() ? archiveSerialNumber()
: this.archiveSerialNumber, : this.archiveSerialNumber,
archivedFileName: archivedFileName ?? this.archivedFileName, 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 => [ List<Object?> get props => [
id, id,
title, title,
content.hashCode, content,
tags,
documentType,
storagePath,
correspondent, correspondent,
documentType,
tags,
storagePath,
created, created,
modified, modified,
added, added,
archiveSerialNumber, archiveSerialNumber,
originalFileName, originalFileName,
archivedFileName, archivedFileName,
storagePath, owner,
userCanChange,
]; ];
} }

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,15 @@ class PaperlessServerStatisticsModel {
: documentsTotal = json['documents_total'] ?? 0, : documentsTotal = json['documents_total'] ?? 0,
documentsInInbox = json['documents_inbox'] ?? 0, documentsInInbox = json['documents_inbox'] ?? 0,
totalChars = json["character_count"], totalChars = json["character_count"],
fileTypeCounts = (json['document_file_type_counts'] as List? ?? []) fileTypeCounts =
.map((e) => DocumentFileTypeCount.fromJson(e)) _parseFileTypeCounts(json['document_file_type_counts']);
.toList();
static List<DocumentFileTypeCount> _parseFileTypeCounts(dynamic value) {
if (value is List) {
return value.map((e) => DocumentFileTypeCount.fromJson(e)).toList();
}
return [];
}
} }
class DocumentFileTypeCount { 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) { } on DioException catch (exception) {
throw exception.unravel( 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:dio/dio.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.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/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/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/request_utils.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