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

@@ -3,30 +3,23 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_bulk_action_state.dart';
part 'document_bulk_action_cubit.freezed.dart';
class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
final PaperlessDocumentsApi _documentsApi;
final LabelRepository _labelRepository;
final DocumentChangedNotifier _notifier;
DocumentBulkActionCubit(
this._documentsApi,
this._labelRepository,
this._notifier, {
required List<DocumentModel> selection,
}) : super(
DocumentBulkActionState(
selection: selection,
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
),
) {
_notifier.addListener(
@@ -42,19 +35,6 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
);
},
);
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
),
);
},
);
}
Future<void> bulkDelete() async {
@@ -69,47 +49,74 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
}
Future<void> bulkModifyCorrespondent(int? correspondentId) async {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.correspondent(
state.selectedIds,
labelId: correspondentId,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(correspondent: () => correspondentId));
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.correspondent(
state.selectedIds,
labelId: correspondentId,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(correspondent: () => correspondentId));
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 {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.documentType(
state.selectedIds,
labelId: documentTypeId,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(documentType: () => documentTypeId));
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.documentType(
state.selectedIds,
labelId: documentTypeId,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(documentType: () => documentTypeId));
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 {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.storagePath(
state.selectedIds,
labelId: storagePathId,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(storagePath: () => storagePathId));
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyLabelAction.storagePath(
state.selectedIds,
labelId: storagePathId,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(storagePath: () => storagePathId));
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> removeTagIds = const [],
}) async {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyTagsAction(
state.selectedIds,
addTags: addTagIds,
removeTags: removeTagIds,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(tags: [
...doc.tags.toSet().difference(removeTagIds.toSet()),
...addTagIds
]));
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
try {
final modifiedDocumentIds = await _documentsApi.bulkAction(
BulkModifyTagsAction(
state.selectedIds,
addTags: addTagIds,
removeTags: removeTagIds,
),
);
final updatedDocuments = state.selection
.where((element) => modifiedDocumentIds.contains(element.id))
.map((doc) => doc.copyWith(tags: [
...doc.tags.toSet().difference(removeTagIds.toSet()),
...addTagIds
]));
for (final doc in updatedDocuments) {
_notifier.notifyUpdated(doc);
}
} on PaperlessApiException catch (e) {
addError(
TransientPaperlessApiError(
code: e.code,
details: e.details,
),
);
}
}
@override
Future<void> close() {
_notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
@@ -33,24 +34,31 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
DateTime? createdAt,
int? asn,
}) async {
final taskId = await _documentApi.create(
bytes,
filename: filename,
title: title,
correspondent: correspondent,
documentType: documentType,
tags: tags,
createdAt: createdAt,
asn: asn,
onProgressChanged: (progress) {
if (!isClosed) {
emit(state.copyWith(uploadProgress: progress));
}
},
);
if (taskId != null) {
_tasksNotifier.listenToTaskChanges(taskId);
try {
final taskId = await _documentApi.create(
bytes,
filename: filename,
title: title,
correspondent: correspondent,
documentType: documentType,
tags: tags,
createdAt: createdAt,
asn: asn,
onProgressChanged: (progress) {
if (!isClosed) {
emit(state.copyWith(uploadProgress: progress));
}
},
);
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
Widget build(BuildContext context) {
final labels = context.watch<LabelRepository>().state;
final labelRepository = context.watch<LabelRepository>();
return BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, state) {
return Scaffold(
@@ -242,7 +242,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: labels.correspondents,
options: labelRepository.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: context
@@ -265,7 +265,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey,
options: labels.documentTypes,
options: labelRepository.documentTypes,
prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
@@ -283,7 +283,7 @@ class _DocumentUploadPreparationPageState
allowCreation: true,
allowExclude: false,
allowOnlySelection: true,
options: labels.tags,
options: labelRepository.tags,
),
Text(
"* " + S.of(context)!.uploadInferValuesHint,
@@ -353,8 +353,6 @@ class _DocumentUploadPreparationPageState
S.of(context)!.documentSuccessfullyUploadedProcessing,
);
context.pop(DocumentUploadResult(true, taskId));
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessFormValidationException catch (exception) {
setState(() => _errors = exception.validationMessages);
} catch (error, stackTrace) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routing/routes/app_logs_route.dart';
import 'package:paperless_mobile/theme.dart';
class LoginTransitionPage extends StatelessWidget {
@@ -20,10 +22,25 @@ class LoginTransitionPage extends StatelessWidget {
body: Stack(
alignment: Alignment.center,
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(
alignment: Alignment.bottomCenter,
child: Text(text).paddedOnly(bottom: 24),
child: TextButton(
child: Text(S.of(context)!.appLogs('')),
onPressed: () {
AppLogsRoute().push(context);
},
),
),
],
).padded(16),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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