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

@@ -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,
),
],
);
},
);
}
}