feat: Add improved date input, fix bugs, restructurings

This commit is contained in:
Anton Stubenbord
2023-10-20 17:28:54 +02:00
parent 18e178b644
commit 652abb6945
32 changed files with 840 additions and 775 deletions

View File

@@ -1,4 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
extension DocumentModelIterableExtension on Iterable<DocumentModel> { extension DocumentModelIterableExtension on Iterable<DocumentModel> {
@@ -16,3 +18,8 @@ extension DocumentModelIterableExtension on Iterable<DocumentModel> {
return whereNot((element) => element.id == document.id); return whereNot((element) => element.id == document.id);
} }
} }
extension SessionAwareDownloadIdExtension on DocumentModel {
String buildThumbnailUrl(BuildContext context) =>
context.read<PaperlessDocumentsApi>().getThumbnailUrl(id);
}

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart'; import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart'; import 'package:paperless_mobile/core/repository/persistent_repository.dart';
@@ -11,14 +10,12 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
LabelRepository(this._api) : super(const LabelRepositoryState()); LabelRepository(this._api) : super(const LabelRepositoryState());
Future<void> initialize() async { Future<void> initialize() async {
await Future.wait([
await Future.wait([ findAllCorrespondents(),
findAllCorrespondents(), findAllDocumentTypes(),
findAllDocumentTypes(), findAllStoragePaths(),
findAllStoragePaths(), findAllTags(),
findAllTags(), ]);
]);
} }
Future<Tag> createTag(Tag object) async { Future<Tag> createTag(Tag object) async {

View File

@@ -1,3 +1,5 @@
// ignore_for_file: invalid_use_of_protected_member
import 'dart:collection'; import 'dart:collection';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@@ -9,6 +11,39 @@ import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart'; import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class FormDateTime {
final int? day;
final int? month;
final int? year;
FormDateTime({this.day, this.month, this.year});
FormDateTime.fromDateTime(DateTime date)
: day = date.day,
month = date.month,
year = date.year;
FormDateTime copyWith({int? day, int? month, int? year}) {
return FormDateTime(
day: day ?? this.day,
month: month ?? this.month,
year: year ?? this.year,
);
}
bool get isComplete => day != null && month != null && year != null;
DateTime? toDateTime() {
if (day == null && month == null && year == null) {
return null;
}
if (!isComplete) {
throw ArgumentError.notNull("day, month and year must be set together");
}
return DateTime(year!, month!, day!);
}
}
/// A localized, segmented date input field. /// A localized, segmented date input field.
class FormBuilderLocalizedDatePicker extends StatefulWidget { class FormBuilderLocalizedDatePicker extends StatefulWidget {
final String name; final String name;
@@ -124,42 +159,35 @@ class _FormBuilderLocalizedDatePickerState
} }
} }
}, },
child: FormBuilderField<DateTime>( child: FormBuilderField<FormDateTime>(
name: widget.name,
validator: _validateDate, validator: _validateDate,
onChanged: (value) { onChanged: (value) {
// We have to temporarily disable our listeners on the TextEditingController here assert(!widget.allowUnset && value != null);
// since otherwise the listeners get notified of the change and if (value == null) {
// the fields get focused and highlighted/selected (as defined in the return;
// listeners above).
_temporarilyDisableListeners = true;
for (var control in _textFieldControls) {
control.controller.text = DateFormat(control.format).format(value!);
} }
_temporarilyDisableListeners = false; // When the change is requested from external sources, such as calling
// field.didChange(value), then we want to update the text fields individually
// without causing the either field to gain focus (as defined above).
final isChangeRequestedFromOutside =
_textFieldControls.none((element) => element.node.hasFocus);
if (isChangeRequestedFromOutside) {
_updateInputsWithDate(value, disableListeners: true);
}
// Imitate the functionality of the validator function in "normal" form fields.
// The error is shown on the outer decorator as if this was a regular text input.
// Errors are cleared after the next user interaction.
final error = _validateDate(value); final error = _validateDate(value);
setState(() { setState(() {
_error = error; _error = error;
}); });
if (value?.isBefore(widget.firstDate) ?? false) {
setState(() => _error = "Date must be after " +
DateFormat.yMd(widget.locale.toString())
.format(widget.firstDate) +
".");
return;
}
if (value?.isAfter(widget.lastDate) ?? false) {
setState(() => _error = "Date must be before " +
DateFormat.yMd(widget.locale.toString())
.format(widget.lastDate) +
".");
return;
}
}, },
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
name: widget.name, initialValue: widget.initialValue != null
initialValue: widget.initialValue, ? FormDateTime.fromDateTime(widget.initialValue!)
: null,
builder: (field) { builder: (field) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -170,7 +198,6 @@ class _FormBuilderLocalizedDatePickerState
decoration: InputDecoration( decoration: InputDecoration(
errorText: _error, errorText: _error,
labelText: widget.labelText, labelText: widget.labelText,
prefixIcon: widget.prefixIcon,
suffixIcon: Row( suffixIcon: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -179,28 +206,33 @@ class _FormBuilderLocalizedDatePickerState
onPressed: () async { onPressed: () async {
final selectedDate = await showDatePicker( final selectedDate = await showDatePicker(
context: context, context: context,
initialDate: widget.initialValue ?? DateTime.now(), initialDate:
field.value?.toDateTime() ?? DateTime.now(),
firstDate: widget.firstDate, firstDate: widget.firstDate,
lastDate: widget.lastDate, lastDate: widget.lastDate,
initialEntryMode: DatePickerEntryMode.calendarOnly, initialEntryMode: DatePickerEntryMode.calendarOnly,
); );
if (selectedDate != null) { if (selectedDate != null) {
_updateInputsWithDate(selectedDate); final formDate =
field.didChange(selectedDate); FormDateTime.fromDateTime(selectedDate);
FocusScope.of(context).unfocus(); _temporarilyDisableListeners = true;
_updateInputsWithDate(formDate);
field.didChange(formDate);
_temporarilyDisableListeners = false;
} }
}, },
), ),
IconButton( if (widget.allowUnset)
onPressed: () { IconButton(
for (var c in _textFieldControls) { onPressed: () {
c.controller.clear(); for (var c in _textFieldControls) {
} c.controller.clear();
_textFieldControls.first.node.requestFocus(); }
field.didChange(null); _textFieldControls.first.node.requestFocus();
}, field.didChange(null);
icon: const Icon(Icons.clear), },
), icon: const Icon(Icons.clear),
),
], ],
).paddedOnly(right: 4), ).paddedOnly(right: 4),
), ),
@@ -220,19 +252,26 @@ class _FormBuilderLocalizedDatePickerState
); );
} }
String? _validateDate(DateTime? date) { String? _validateDate(FormDateTime? date) {
if (widget.allowUnset && date == null) { if (widget.allowUnset && date == null) {
return null; return null;
} }
if (date == null) { if (date == null) {
return S.of(context)!.thisFieldIsRequired; return S.of(context)!.thisFieldIsRequired;
} }
if (date.isBefore(widget.firstDate)) { final d = date.toDateTime();
if (d == null) {
return S.of(context)!.thisFieldIsRequired;
}
if (d.day != date.day && d.month != date.month && d.year != date.year) {
return "Invalid date.";
}
if (d.isBefore(widget.firstDate)) {
final formattedDateHint = final formattedDateHint =
DateFormat.yMd(widget.locale.toString()).format(widget.firstDate); DateFormat.yMd(widget.locale.toString()).format(widget.firstDate);
return "Date must be after $formattedDateHint."; return "Date must be after $formattedDateHint.";
} }
if (date.isAfter(widget.lastDate)) { if (d.isAfter(widget.lastDate)) {
final formattedDateHint = final formattedDateHint =
DateFormat.yMd(widget.locale.toString()).format(widget.lastDate); DateFormat.yMd(widget.locale.toString()).format(widget.lastDate);
return "Date must be before $formattedDateHint."; return "Date must be before $formattedDateHint.";
@@ -240,30 +279,31 @@ class _FormBuilderLocalizedDatePickerState
return null; return null;
} }
void _updateInputsWithDate(DateTime date) { void _updateInputsWithDate(
final components = _format.split(_separator); FormDateTime date, {
for (int i = 0; i < components.length; i++) { bool disableListeners = false,
final formatString = components[i]; }) {
final value = DateFormat(formatString).format(date); if (disableListeners) {
_textFieldControls.elementAt(i).controller.text = value; _temporarilyDisableListeners = true;
} }
for (var controls in _textFieldControls) {
final value = DateFormat(controls.format).format(date.toDateTime()!);
controls.controller.text = value;
}
_temporarilyDisableListeners = false;
} }
Widget _buildDateSegmentInput( Widget _buildDateSegmentInput(
_NeighbourAwareDateInputSegmentControls controls, _NeighbourAwareDateInputSegmentControls controls,
BuildContext context, BuildContext context,
FormFieldState<DateTime> field, FormFieldState<FormDateTime> field,
) { ) {
return TextFormField( return TextFormField(
onFieldSubmitted: (value) { onFieldSubmitted: (value) {
if (value.length < controls.format.length) { if (value.length < controls.format.length) {
controls.controller.text = value.padLeft(controls.format.length, '0'); controls.controller.text = value.padLeft(controls.format.length, '0');
} }
_textFieldControls controls.next?.node.requestFocus();
.elementAt(controls.position)
.next
?.node
.requestFocus();
}, },
style: const TextStyle(fontFamily: 'RobotoMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
keyboardType: TextInputType.datetime, keyboardType: TextInputType.datetime,
@@ -275,17 +315,18 @@ class _FormBuilderLocalizedDatePickerState
maxLengthEnforcement: MaxLengthEnforcement.enforced, maxLengthEnforcement: MaxLengthEnforcement.enforced,
enableInteractiveSelection: false, enableInteractiveSelection: false,
onChanged: (value) { onChanged: (value) {
if (value.length == controls.format.length && field.value != null) { if (value.length == controls.format.length) {
final number = int.tryParse(value); final number = int.tryParse(value);
if (number == null) { if (number == null) {
return; return;
} }
final fieldValue = field.value ?? FormDateTime();
final newValue = switch (controls.type) { final newValue = switch (controls.type) {
_DateInputSegment.day => field.value!.copyWith(day: number), _DateInputSegment.day => fieldValue.copyWith(day: number),
_DateInputSegment.month => field.value!.copyWith(month: number), _DateInputSegment.month => fieldValue.copyWith(month: number),
_DateInputSegment.year => field.value!.copyWith(year: number), _DateInputSegment.year => fieldValue.copyWith(year: number),
}; };
field.didChange(newValue); field.setValue(newValue);
} }
}, },
inputFormatters: [ inputFormatters: [
@@ -299,6 +340,12 @@ class _FormBuilderLocalizedDatePickerState
}, },
), ),
], ],
onEditingComplete: () {
if (field.value != null) {
_updateInputsWithDate(field.value!, disableListeners: true);
}
FocusScope.of(context).unfocus();
},
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
suffixIcon: controls.position < 2 suffixIcon: controls.position < 2

View File

@@ -2,23 +2,23 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:path/path.dart' as p;
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:cross_file/cross_file.dart';
import 'package:path/path.dart' as p;
part 'document_details_cubit.freezed.dart';
part 'document_details_state.dart'; part 'document_details_state.dart';
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> { class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final int id;
final PaperlessDocumentsApi _api; final PaperlessDocumentsApi _api;
final DocumentChangedNotifier _notifier; final DocumentChangedNotifier _notifier;
final LocalNotificationService _notificationService; final LocalNotificationService _notificationService;
@@ -29,24 +29,46 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
this._labelRepository, this._labelRepository,
this._notifier, this._notifier,
this._notificationService, { this._notificationService, {
required DocumentModel initialDocument, required this.id,
}) : super(DocumentDetailsState(document: initialDocument)) { }) : super(const DocumentDetailsInitial()) {
_notifier.addListener(this, onUpdated: (document) { _notifier.addListener(this, onUpdated: (document) {
if (document.id == state.document.id) { if (state is DocumentDetailsLoaded) {
replace(document); final currentState = state as DocumentDetailsLoaded;
if (document.id == currentState.document.id) {
replace(document);
}
} }
}); });
_labelRepository.addListener( }
this,
onChanged: (labels) => emit( Future<void> initialize() async {
state.copyWith( debugPrint("Initialize called");
correspondents: labels.correspondents, emit(const DocumentDetailsLoading());
documentTypes: labels.documentTypes, try {
tags: labels.tags, final (document, metaData) = await Future.wait([
storagePaths: labels.storagePaths, _api.find(id),
), _api.getMetaData(id),
), ]).then((value) => (
); value[0] as DocumentModel,
value[1] as DocumentMetaData,
));
// final document = await _api.find(id);
// final metaData = await _api.getMetaData(id);
debugPrint("Document data loaded for $id");
emit(DocumentDetailsLoaded(
document: document,
metaData: metaData,
));
} catch (error, stackTrace) {
logger.fe(
"An error occurred while loading data for document $id.",
className: runtimeType.toString(),
methodName: 'initialize',
error: error,
stackTrace: stackTrace,
);
emit(const DocumentDetailsError());
}
} }
Future<void> delete(DocumentModel document) async { Future<void> delete(DocumentModel document) async {
@@ -54,20 +76,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
_notifier.notifyDeleted(document); _notifier.notifyDeleted(document);
} }
Future<void> loadMetaData() async {
final metaData = await _api.getMetaData(state.document);
if (!isClosed) {
emit(state.copyWith(metaData: metaData));
}
}
Future<void> loadFullContent() async {
await Future.delayed(const Duration(seconds: 5));
final doc = await _api.find(state.document.id);
_notifier.notifyUpdated(doc);
emit(state.copyWith(isFullContentLoaded: true));
}
Future<void> assignAsn( Future<void> assignAsn(
DocumentModel document, { DocumentModel document, {
int? asn, int? asn,
@@ -87,11 +95,15 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
} }
Future<ResultType> openDocumentInSystemViewer() async { Future<ResultType> openDocumentInSystemViewer() async {
final cacheDir = FileService.instance.temporaryDirectory; final s = state;
if (state.metaData == null) { if (s is! DocumentDetailsLoaded) {
await loadMetaData(); throw Exception(
"Document cannot be opened in system viewer "
"if document information has not yet been loaded.",
);
} }
final filePath = state.metaData!.mediaFilename.replaceAll("/", " "); final cacheDir = FileService.instance.temporaryDirectory;
final filePath = s.metaData.mediaFilename.replaceAll("/", " ");
final fileName = "${p.basenameWithoutExtension(filePath)}.pdf"; final fileName = "${p.basenameWithoutExtension(filePath)}.pdf";
final file = File("${cacheDir.path}/$fileName"); final file = File("${cacheDir.path}/$fileName");
@@ -99,7 +111,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (!file.existsSync()) { if (!file.existsSync()) {
file.createSync(); file.createSync();
await _api.downloadToFile( await _api.downloadToFile(
state.document, s.document,
file.path, file.path,
); );
} }
@@ -110,7 +122,14 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
} }
void replace(DocumentModel document) { void replace(DocumentModel document) {
emit(state.copyWith(document: document)); final s = state;
if (s is! DocumentDetailsLoaded) {
return;
}
emit(DocumentDetailsLoaded(
document: document,
metaData: s.metaData,
));
} }
Future<void> downloadDocument({ Future<void> downloadDocument({
@@ -118,10 +137,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
required String locale, required String locale,
required String userId, required String userId,
}) async { }) async {
if (state.metaData == null) { final s = state;
await loadMetaData(); if (s is! DocumentDetailsLoaded) {
return;
} }
String targetPath = _buildDownloadFilePath( String targetPath = _buildDownloadFilePath(
s.metaData,
downloadOriginal, downloadOriginal,
FileService.instance.downloadsDirectory, FileService.instance.downloadsDirectory,
); );
@@ -130,7 +151,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
await File(targetPath).create(); await File(targetPath).create();
} else { } else {
await _notificationService.notifyDocumentDownload( await _notificationService.notifyDocumentDownload(
document: state.document, document: s.document,
filename: p.basename(targetPath), filename: p.basename(targetPath),
filePath: targetPath, filePath: targetPath,
finished: true, finished: true,
@@ -149,12 +170,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
// ); // );
await _api.downloadToFile( await _api.downloadToFile(
state.document, s.document,
targetPath, targetPath,
original: downloadOriginal, original: downloadOriginal,
onProgressChanged: (progress) { onProgressChanged: (progress) {
_notificationService.notifyDocumentDownload( _notificationService.notifyDocumentDownload(
document: state.document, document: s.document,
filename: p.basename(targetPath), filename: p.basename(targetPath),
filePath: targetPath, filePath: targetPath,
finished: true, finished: true,
@@ -165,26 +186,28 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
}, },
); );
await _notificationService.notifyDocumentDownload( await _notificationService.notifyDocumentDownload(
document: state.document, document: s.document,
filename: p.basename(targetPath), filename: p.basename(targetPath),
filePath: targetPath, filePath: targetPath,
finished: true, finished: true,
locale: locale, locale: locale,
userId: userId, userId: userId,
); );
logger.fi("Document '${state.document.title}' saved to $targetPath."); logger.fi("Document '${s.document.title}' saved to $targetPath.");
} }
Future<void> shareDocument({bool shareOriginal = false}) async { Future<void> shareDocument({bool shareOriginal = false}) async {
if (state.metaData == null) { final s = state;
await loadMetaData(); if (s is! DocumentDetailsLoaded) {
return;
} }
String filePath = _buildDownloadFilePath( String filePath = _buildDownloadFilePath(
s.metaData,
shareOriginal, shareOriginal,
FileService.instance.temporaryDirectory, FileService.instance.temporaryDirectory,
); );
await _api.downloadToFile( await _api.downloadToFile(
state.document, s.document,
filePath, filePath,
original: shareOriginal, original: shareOriginal,
); );
@@ -192,23 +215,27 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
[ [
XFile( XFile(
filePath, filePath,
name: state.document.originalFileName, name: s.document.originalFileName,
mimeType: "application/pdf", mimeType: "application/pdf",
lastModified: state.document.modified, lastModified: s.document.modified,
), ),
], ],
subject: state.document.title, subject: s.document.title,
); );
} }
Future<void> printDocument() async { Future<void> printDocument() async {
if (state.metaData == null) { final s = state;
await loadMetaData(); if (s is! DocumentDetailsLoaded) {
return;
} }
final filePath = final filePath = _buildDownloadFilePath(
_buildDownloadFilePath(false, FileService.instance.temporaryDirectory); s.metaData,
false,
FileService.instance.temporaryDirectory,
);
await _api.downloadToFile( await _api.downloadToFile(
state.document, s.document,
filePath, filePath,
original: false, original: false,
); );
@@ -217,13 +244,14 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
throw Exception("An error occurred while downloading the document."); throw Exception("An error occurred while downloading the document.");
} }
Printing.layoutPdf( Printing.layoutPdf(
name: state.document.title, name: s.document.title,
onLayout: (format) => file.readAsBytesSync(), onLayout: (format) => file.readAsBytesSync(),
); );
} }
String _buildDownloadFilePath(bool original, Directory dir) { String _buildDownloadFilePath(
final normalizedPath = state.metaData!.mediaFilename.replaceAll("/", " "); DocumentMetaData meta, bool original, Directory dir) {
final normalizedPath = meta.mediaFilename.replaceAll("/", " ");
final extension = original ? p.extension(normalizedPath) : '.pdf'; final extension = original ? p.extension(normalizedPath) : '.pdf';
return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension"; return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension";
} }

View File

@@ -1,14 +1,41 @@
part of 'document_details_cubit.dart'; part of 'document_details_cubit.dart';
@freezed sealed class DocumentDetailsState {
class DocumentDetailsState with _$DocumentDetailsState { const 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;
} }
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

@@ -2,20 +2,18 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
@@ -29,13 +27,21 @@ import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/theme.dart';
class DocumentDetailsPage extends StatefulWidget { class DocumentDetailsPage extends StatefulWidget {
final int id;
final String? title;
final bool isLabelClickable; final bool isLabelClickable;
final String? titleAndContentQueryString; final String? titleAndContentQueryString;
final String? thumbnailUrl;
final String? heroTag;
const DocumentDetailsPage({ const DocumentDetailsPage({
Key? key, Key? key,
this.isLabelClickable = true, this.isLabelClickable = true,
this.titleAndContentQueryString, this.titleAndContentQueryString,
this.thumbnailUrl,
required this.id,
this.heroTag,
this.title,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -57,152 +63,157 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final hasMultiUserSupport = final hasMultiUserSupport =
context.watch<LocalUserAccount>().hasMultiUserSupport; context.watch<LocalUserAccount>().hasMultiUserSupport;
final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0); final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0);
final title = context.watch<DocumentDetailsCubit>().state.document.title;
return AnnotatedRegion( return AnnotatedRegion(
value: buildOverlayStyle( value: buildOverlayStyle(
Theme.of(context), Theme.of(context),
systemNavigationBarColor: Theme.of(context).bottomAppBarTheme.color, systemNavigationBarColor: Theme.of(context).bottomAppBarTheme.color,
), ),
child: WillPopScope( child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
onWillPop: () async { builder: (context, state) {
Navigator.of(context) return DefaultTabController(
.pop(context.read<DocumentDetailsCubit>().state.document); length: tabLength,
return false;
},
child: DefaultTabController(
length: tabLength,
child: BlocListener<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) =>
!previous.isConnected && current.isConnected,
listener: (context, state) {
context.read<DocumentDetailsCubit>().loadMetaData();
},
child: Scaffold( child: Scaffold(
extendBodyBehindAppBar: false, extendBodyBehindAppBar: false,
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked, FloatingActionButtonLocation.endDocked,
floatingActionButton: _buildEditButton(), floatingActionButton: switch (state) {
DocumentDetailsLoaded(document: var document) =>
_buildEditButton(document),
_ => null
},
bottomNavigationBar: _buildBottomAppBar(), bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context), context),
sliver: SliverAppBar( sliver:
title: Text(title), BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
leading: const BackButton(), builder: (context, state) {
pinned: true, final title = switch (state) {
forceElevated: innerBoxIsScrolled, DocumentDetailsLoaded(document: var document) =>
collapsedHeight: kToolbarHeight, document.title,
expandedHeight: 250.0, _ => widget.title ?? '',
flexibleSpace: FlexibleSpaceBar( };
background: BlocBuilder<DocumentDetailsCubit, return SliverAppBar(
DocumentDetailsState>( title: Text(title),
builder: (context, state) { leading: const BackButton(),
return Hero( pinned: true,
tag: "thumb_${state.document.id}", forceElevated: innerBoxIsScrolled,
child: GestureDetector( collapsedHeight: kToolbarHeight,
onTap: () { expandedHeight: 250.0,
DocumentPreviewRoute($extra: state.document) flexibleSpace: FlexibleSpaceBar(
.push(context); background: Builder(
}, builder: (context) {
child: Stack( return Hero(
alignment: Alignment.topCenter, tag: widget.heroTag ?? "thumb_${widget.id}",
children: [ child: GestureDetector(
Positioned.fill( onTap: () {
child: DocumentPreview( DocumentPreviewRoute(
enableHero: false, id: widget.id,
document: state.document, title: title,
fit: BoxFit.cover, ).push(context);
alignment: Alignment.topCenter, },
), child: Stack(
), alignment: Alignment.topCenter,
Positioned.fill( children: [
child: DecoratedBox( Positioned.fill(
decoration: BoxDecoration( child: DocumentPreview(
gradient: LinearGradient( documentId: widget.id,
stops: [0.2, 0.4], title: title,
colors: [ enableHero: false,
Theme.of(context) fit: BoxFit.cover,
.colorScheme alignment: Alignment.topCenter,
.background
.withOpacity(0.6),
Theme.of(context)
.colorScheme
.background
.withOpacity(0.3),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
), ),
), ),
), Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
stops: [0.2, 0.4],
colors: [
Theme.of(context)
.colorScheme
.background
.withOpacity(0.6),
Theme.of(context)
.colorScheme
.background
.withOpacity(0.3),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
),
],
), ),
], ),
), );
), },
);
},
),
),
bottom: ColoredTabBar(
tabBar: TabBar(
isScrollable: true,
tabs: [
Tab(
child: Text(
S.of(context)!.overview,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
), ),
Tab( ),
child: Text( bottom: ColoredTabBar(
S.of(context)!.content, tabBar: TabBar(
style: TextStyle( isScrollable: true,
color: Theme.of(context) tabs: [
.colorScheme Tab(
.onPrimaryContainer, child: Text(
), S.of(context)!.overview,
), style: TextStyle(
), color: Theme.of(context)
Tab( .colorScheme
child: Text( .onPrimaryContainer,
S.of(context)!.metaData, ),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
Tab(
child: Text(
S.of(context)!.similarDocuments,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (hasMultiUserSupport && false)
Tab(
child: Text(
"Permissions",
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
), ),
), ),
), Tab(
], child: Text(
), S.of(context)!.content,
), style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
Tab(
child: Text(
S.of(context)!.metaData,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
Tab(
child: Text(
S.of(context)!.similarDocuments,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
// if (hasMultiUserSupport && false)
// Tab(
// child: Text(
// "Permissions",
// style: TextStyle(
// color: Theme.of(context)
// .colorScheme
// .onPrimaryContainer,
// ),
// ),
// ),
],
),
),
);
},
), ),
), ),
], ],
@@ -214,7 +225,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
documentId: state.document.id, documentId: widget.id,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -229,12 +240,19 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context), .sliverOverlapAbsorberHandleFor(context),
), ),
DocumentOverviewWidget( switch (state) {
document: state.document, DocumentDetailsLoaded(
itemSpacing: _itemSpacing, document: var document
queryString: ) =>
widget.titleAndContentQueryString, DocumentOverviewWidget(
), document: document,
itemSpacing: _itemSpacing,
queryString:
widget.titleAndContentQueryString,
),
DocumentDetailsError() => _buildErrorState(),
_ => _buildLoadingState(),
},
], ],
), ),
CustomScrollView( CustomScrollView(
@@ -243,13 +261,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context), .sliverOverlapAbsorberHandleFor(context),
), ),
DocumentContentWidget( switch (state) {
isFullContentLoaded: DocumentDetailsLoaded(
state.isFullContentLoaded, document: var document
document: state.document, ) =>
queryString: DocumentContentWidget(
widget.titleAndContentQueryString, document: document,
), queryString:
widget.titleAndContentQueryString,
),
DocumentDetailsError() => _buildErrorState(),
_ => _buildLoadingState(),
}
], ],
), ),
CustomScrollView( CustomScrollView(
@@ -258,10 +281,19 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context), .sliverOverlapAbsorberHandleFor(context),
), ),
DocumentMetaDataWidget( switch (state) {
document: state.document, DocumentDetailsLoaded(
itemSpacing: _itemSpacing, document: var document,
), metaData: var metaData,
) =>
DocumentMetaDataWidget(
document: document,
itemSpacing: _itemSpacing,
metaData: metaData,
),
DocumentDetailsError() => _buildErrorState(),
_ => _buildLoadingState(),
},
], ],
), ),
CustomScrollView( CustomScrollView(
@@ -277,20 +309,20 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
], ],
), ),
if (hasMultiUserSupport && false) // if (hasMultiUserSupport && false)
CustomScrollView( // CustomScrollView(
controller: _pagingScrollController, // controller: _pagingScrollController,
slivers: [ // slivers: [
SliverOverlapInjector( // SliverOverlapInjector(
handle: NestedScrollView // handle: NestedScrollView
.sliverOverlapAbsorberHandleFor( // .sliverOverlapAbsorberHandleFor(
context), // context),
), // ),
DocumentPermissionsWidget( // DocumentPermissionsWidget(
document: state.document, // document: state.document,
), // ),
], // ],
), // ),
], ],
), ),
), ),
@@ -299,13 +331,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
), ),
), ),
), );
), },
), ),
); );
} }
Widget _buildEditButton() { Widget _buildEditButton(DocumentModel document) {
final currentUser = context.watch<LocalUserAccount>(); final currentUser = context.watch<LocalUserAccount>();
bool canEdit = context.watchInternetConnection && bool canEdit = context.watchInternetConnection &&
@@ -313,7 +345,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
if (!canEdit) { if (!canEdit) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final document = context.read<DocumentDetailsCubit>().state.document;
return Tooltip( return Tooltip(
message: S.of(context)!.editDocumentTooltip, message: S.of(context)!.editDocumentTooltip,
preferBelow: false, preferBelow: false,
@@ -326,60 +357,80 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
); );
} }
Widget _buildErrorState() {
return SliverToBoxAdapter(
child: Center(
child: Text("Could not load document."),
),
);
}
Widget _buildLoadingState() {
return SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(),
),
);
}
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildBottomAppBar() { BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildBottomAppBar() {
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) { builder: (context, state) {
final currentUser = context.watch<LocalUserAccount>();
return BottomAppBar( return BottomAppBar(
child: BlocBuilder<ConnectivityCubit, ConnectivityState>( child: Builder(
builder: (context, connectivityState) { builder: (context) {
final currentUser = context.watch<LocalUserAccount>(); return switch (state) {
return Row( DocumentDetailsLoaded(document: var document) => Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
ConnectivityAwareActionWrapper( ConnectivityAwareActionWrapper(
disabled: !currentUser.paperlessUser.canDeleteDocuments, disabled: !currentUser.paperlessUser.canDeleteDocuments,
offlineBuilder: (context, child) { offlineBuilder: (context, child) {
return const IconButton( return const IconButton(
icon: Icon(Icons.delete), icon: Icon(Icons.delete),
onPressed: null, onPressed: null,
).paddedSymmetrically(horizontal: 4); ).paddedSymmetrically(horizontal: 4);
}, },
child: IconButton( child: IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip, tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () => _onDelete(state.document), onPressed: () => _onDelete(document),
).paddedSymmetrically(horizontal: 4), ).paddedSymmetrically(horizontal: 4),
),
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) =>
const DocumentDownloadButton(
document: null,
enabled: false,
),
child: DocumentDownloadButton(
document: document,
),
),
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => const IconButton(
icon: Icon(Icons.open_in_new),
onPressed: null,
),
child: IconButton(
tooltip: S.of(context)!.openInSystemViewer,
icon: const Icon(Icons.open_in_new),
onPressed: _onOpenFileInSystemViewer,
).paddedOnly(right: 4.0),
),
DocumentShareButton(document: document),
IconButton(
tooltip: S.of(context)!.print,
onPressed: () => context
.read<DocumentDetailsCubit>()
.printDocument(),
icon: const Icon(Icons.print),
),
],
), ),
ConnectivityAwareActionWrapper( _ => SizedBox.shrink(),
offlineBuilder: (context, child) => };
const DocumentDownloadButton(
document: null,
enabled: false,
),
child: DocumentDownloadButton(
document: state.document,
),
),
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => const IconButton(
icon: Icon(Icons.open_in_new),
onPressed: null,
),
child: IconButton(
tooltip: S.of(context)!.openInSystemViewer,
icon: const Icon(Icons.open_in_new),
onPressed: _onOpenFileInSystemViewer,
).paddedOnly(right: 4.0),
),
DocumentShareButton(document: state.document),
IconButton(
tooltip: S.of(context)!.print,
onPressed: () =>
context.read<DocumentDetailsCubit>().printDocument(),
icon: const Icon(Icons.print),
),
],
);
}, },
), ),
); );
@@ -423,11 +474,4 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
} }
} }
} }
Future<void> _onOpen(DocumentModel document) async {
DocumentPreviewRoute(
$extra: document,
title: document.title,
).push(context);
}
} }

View File

@@ -50,11 +50,16 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments; context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>( return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous is DocumentDetailsLoaded &&
current is DocumentDetailsLoaded &&
previous.document.archiveSerialNumber != previous.document.archiveSerialNumber !=
current.document.archiveSerialNumber, current.document.archiveSerialNumber,
listener: (context, state) { listener: (context, state) {
_asnEditingController.text = _asnEditingController.text = (state as DocumentDetailsLoaded)
state.document.archiveSerialNumber?.toString() ?? ''; .document
.archiveSerialNumber
?.toString() ??
'';
setState(() { setState(() {
_canUpdate = false; _canUpdate = false;
}); });

View File

@@ -1,26 +1,37 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class DocumentContentWidget extends StatelessWidget { class DocumentContentWidget extends StatelessWidget {
final bool isFullContentLoaded;
final String? queryString;
final DocumentModel document; final DocumentModel document;
final String? queryString;
const DocumentContentWidget({ const DocumentContentWidget({
super.key, super.key,
required this.isFullContentLoaded,
required this.document, required this.document,
this.queryString, this.queryString,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.sizeOf(context).width; // if (document == null) {
// final widths = [.3, .8, .9, .7, .6, .4, .8, .8, .6, .4];
// return SliverToBoxAdapter(
// child: ShimmerPlaceholder(
// child: Column(
// children: [
// for (int i = 0; i < 10; i++)
// Container(
// width: MediaQuery.sizeOf(context).width * widths[i],
// height: 14,
// color: Colors.white,
// margin: EdgeInsets.symmetric(vertical: 4),
// ),
// ],
// ),
// ),
// );
// }
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -31,21 +42,6 @@ class DocumentContentWidget extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
caseSensitive: false, caseSensitive: false,
), ),
if (!isFullContentLoaded)
ShimmerPlaceholder(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var scale in [0.5, 0.9, 0.5, 0.8, 0.9, 0.9])
Container(
margin: const EdgeInsets.symmetric(vertical: 4),
width: screenWidth * scale,
height: 14,
color: Colors.white,
),
],
),
).paddedOnly(top: 4),
], ],
), ),
); );

View File

@@ -4,87 +4,73 @@ import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:paperless_mobile/helpers/format_helpers.dart';
class DocumentMetaDataWidget extends StatefulWidget { class DocumentMetaDataWidget extends StatelessWidget {
final DocumentModel document; final DocumentModel document;
final DocumentMetaData metaData;
final double itemSpacing; final double itemSpacing;
const DocumentMetaDataWidget({ const DocumentMetaDataWidget({
super.key, super.key,
required this.document, required this.document,
required this.metaData,
required this.itemSpacing, required this.itemSpacing,
}); });
@override
State<DocumentMetaDataWidget> createState() => _DocumentMetaDataWidgetState();
}
class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser; final currentUser = context.watch<LocalUserAccount>().paperlessUser;
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) { return SliverList(
if (state.metaData == null) { delegate: SliverChildListDelegate(
return const SliverToBoxAdapter( [
child: Center( if (currentUser.canEditDocuments)
child: CircularProgressIndicator(), ArchiveSerialNumberField(
), document: document,
); ).paddedOnly(bottom: itemSpacing),
} DetailsItem.text(
return SliverList( DateFormat.yMMMMd(Localizations.localeOf(context).toString())
delegate: SliverChildListDelegate( .format(document.modified),
[ context: context,
if (currentUser.canEditDocuments) label: S.of(context)!.modifiedAt,
ArchiveSerialNumberField( ).paddedOnly(bottom: itemSpacing),
document: widget.document, DetailsItem.text(
).paddedOnly(bottom: widget.itemSpacing), DateFormat.yMMMMd(Localizations.localeOf(context).toString())
DetailsItem.text( .format(document.added),
DateFormat.yMMMMd(Localizations.localeOf(context).toString()) context: context,
.format(widget.document.modified), label: S.of(context)!.addedAt,
context: context, ).paddedOnly(bottom: itemSpacing),
label: S.of(context)!.modifiedAt, DetailsItem.text(
).paddedOnly(bottom: widget.itemSpacing), metaData.mediaFilename,
DetailsItem.text( context: context,
DateFormat.yMMMMd(Localizations.localeOf(context).toString()) label: S.of(context)!.mediaFilename,
.format(widget.document.added), ).paddedOnly(bottom: itemSpacing),
context: context, if (document.originalFileName != null)
label: S.of(context)!.addedAt, DetailsItem.text(
).paddedOnly(bottom: widget.itemSpacing), document.originalFileName!,
DetailsItem.text( context: context,
state.metaData!.mediaFilename, label: S.of(context)!.originalMD5Checksum,
context: context, ).paddedOnly(bottom: itemSpacing),
label: S.of(context)!.mediaFilename, DetailsItem.text(
).paddedOnly(bottom: widget.itemSpacing), metaData.originalChecksum,
if (state.document.originalFileName != null) context: context,
DetailsItem.text( label: S.of(context)!.originalMD5Checksum,
state.document.originalFileName!, ).paddedOnly(bottom: itemSpacing),
context: context, DetailsItem.text(
label: S.of(context)!.originalMD5Checksum, formatBytes(metaData.originalSize, 2),
).paddedOnly(bottom: widget.itemSpacing), context: context,
DetailsItem.text( label: S.of(context)!.originalFileSize,
state.metaData!.originalChecksum, ).paddedOnly(bottom: itemSpacing),
context: context, DetailsItem.text(
label: S.of(context)!.originalMD5Checksum, metaData.originalMimeType,
).paddedOnly(bottom: widget.itemSpacing), context: context,
DetailsItem.text( label: S.of(context)!.originalMIMEType,
formatBytes(state.metaData!.originalSize, 2), ).paddedOnly(bottom: itemSpacing),
context: context, ],
label: S.of(context)!.originalFileSize, ),
).paddedOnly(bottom: widget.itemSpacing),
DetailsItem.text(
state.metaData!.originalMimeType,
context: context,
label: S.of(context)!.originalMIMEType,
).paddedOnly(bottom: widget.itemSpacing),
],
),
);
},
); );
} }
} }

View File

@@ -6,6 +6,7 @@ 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/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
@@ -27,6 +28,7 @@ class DocumentOverviewWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = context.watch<LocalUserAccount>().paperlessUser; final user = context.watch<LocalUserAccount>().paperlessUser;
final availableLabels = context.watch<LabelRepository>().state; final availableLabels = context.watch<LabelRepository>().state;
return SliverList.list( return SliverList.list(
children: [ children: [
if (document.title.isNotEmpty) if (document.title.isNotEmpty)

View File

@@ -27,22 +27,6 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
emit(state.copyWith(document: doc)); emit(state.copyWith(document: doc));
} }
}); });
_labelRepository.addListener(
this,
onChanged: (labels) {
if (isClosed) {
return;
}
emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
),
);
},
);
} }
Future<void> updateDocument(DocumentModel document) async { Future<void> updateDocument(DocumentModel document) async {
@@ -76,7 +60,6 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
@override @override
Future<void> close() { Future<void> close() {
_notifier.removeListener(this); _notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close(); return super.close();
} }
} }

View File

@@ -5,9 +5,5 @@ class DocumentEditState with _$DocumentEditState {
const factory DocumentEditState({ const factory DocumentEditState({
required DocumentModel document, required DocumentModel document,
FieldSuggestions? suggestions, FieldSuggestions? suggestions,
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, StoragePath> storagePaths,
@Default({}) Map<int, Tag> tags,
}) = _DocumentEditState; }) = _DocumentEditState;
} }

View File

@@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart'; import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart';
@@ -45,39 +46,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser; final currentUser = context.watch<LocalUserAccount>().paperlessUser;
return BlocConsumer<DocumentEditCubit, DocumentEditState>( return BlocBuilder<DocumentEditCubit, DocumentEditState>(
listenWhen: (previous, current) =>
previous.document.content != current.document.content,
listener: (context, state) {
final contentField = _formKey.currentState?.fields[fkContent];
if (contentField == null) {
return;
}
if (contentField.isDirty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
//TODO: INTL
title: Text("Content has changed!"),
content: Text(
"The content of this document has changed. This can happen if the full content was not yet loaded. By accepting the incoming changes, your changes will be overwritten and therefore lost! Do you want to discard your changes in favor of the full content?",
),
actions: [
DialogCancelButton(),
ElevatedButton(
onPressed: () {
contentField.didChange(state.document.content);
Navigator.of(context).pop();
},
child: Text(S.of(context)!.discard),
),
],
),
);
} else {
contentField.didChange(state.document.content);
}
},
builder: (context, state) { builder: (context, state) {
final filteredSuggestions = state.suggestions; final filteredSuggestions = state.suggestions;
return PopWithUnsavedChanges( return PopWithUnsavedChanges(
@@ -160,7 +129,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
S.of(context)!.addCorrespondent, S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent, labelText: S.of(context)!.correspondent,
options: context options: context
.watch<DocumentEditCubit>() .watch<LabelRepository>()
.state .state
.correspondents, .correspondents,
initialValue: state initialValue: state
@@ -203,7 +172,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
? SetIdQueryParameter( ? SetIdQueryParameter(
id: state.document.documentType!) id: state.document.documentType!)
: const UnsetIdQueryParameter(), : const UnsetIdQueryParameter(),
options: state.documentTypes, options: context
.watch<LabelRepository>()
.state
.documentTypes,
name: _DocumentEditPageState.fkDocumentType, name: _DocumentEditPageState.fkDocumentType,
prefixIcon: prefixIcon:
const Icon(Icons.description_outlined), const Icon(Icons.description_outlined),
@@ -230,7 +202,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
currentUser.canCreateStoragePaths, currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath, addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath, labelText: S.of(context)!.storagePath,
options: state.storagePaths, options: context
.watch<LabelRepository>()
.state
.storagePaths,
initialValue: initialValue:
state.document.storagePath != null state.document.storagePath != null
? SetIdQueryParameter( ? SetIdQueryParameter(
@@ -246,7 +221,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
// Tag form field // Tag form field
if (currentUser.canViewTags) if (currentUser.canViewTags)
TagsFormField( TagsFormField(
options: state.tags, options:
context.watch<LabelRepository>().state.tags,
name: fkTags, name: fkTags,
allowOnlySelection: true, allowOnlySelection: true,
allowCreation: true, allowCreation: true,
@@ -290,30 +266,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
); );
} }
bool _isFieldDirty(DocumentModel document) {
final fkState = _formKey.currentState;
if (fkState == null) {
return false;
}
fkState.save();
final (
title,
correspondent,
documentType,
storagePath,
tags,
createdAt,
content
) = _currentValues;
return document.title != title ||
document.correspondent != correspondent ||
document.documentType != documentType ||
document.storagePath != storagePath ||
const UnorderedIterableEquality().equals(document.tags, tags) ||
document.created != createdAt ||
document.content != content;
}
( (
String? title, String? title,
int? correspondent, int? correspondent,
@@ -333,7 +285,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
fkState.getRawValue<IdQueryParameter?>(fkStoragePath); fkState.getRawValue<IdQueryParameter?>(fkStoragePath);
final tagsParam = fkState.getRawValue<TagsQuery?>(fkTags); final tagsParam = fkState.getRawValue<TagsQuery?>(fkTags);
final title = fkState.getRawValue<String?>(fkTitle); final title = fkState.getRawValue<String?>(fkTitle);
final created = fkState.getRawValue<DateTime?>(fkCreatedDate); final created = fkState.getRawValue<FormDateTime?>(fkCreatedDate);
final correspondent = switch (correspondentParam) { final correspondent = switch (correspondentParam) {
SetIdQueryParameter(id: var id) => id, SetIdQueryParameter(id: var id) => id,
_ => null, _ => null,
@@ -358,7 +310,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
documentType, documentType,
storagePath, storagePath,
tags, tags,
created, created?.toDateTime(),
content content
); );
} }
@@ -432,7 +384,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
DateFormat.yMMMMd(Localizations.localeOf(context).toString()) DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(itemData)), .format(itemData)),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate] onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
?.didChange(itemData), ?.didChange(FormDateTime.fromDateTime(itemData)),
), ),
), ),
], ],

View File

@@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart'; import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
@@ -219,8 +220,12 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
enableHeroAnimation: false, enableHeroAnimation: false,
onTap: (document) { onTap: (document) {
DocumentDetailsRoute($extra: document, isLabelClickable: false) DocumentDetailsRoute(
.push(context); title: document.title,
id: document.id,
isLabelClickable: false,
thumbnailUrl: document.buildThumbnailUrl(context),
).push(context);
}, },
) )
], ],

View File

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
@@ -21,18 +20,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
this._documentApi, this._documentApi,
this._connectivityStatusService, this._connectivityStatusService,
this._tasksNotifier, this._tasksNotifier,
) : super(const DocumentUploadState()) { ) : super(const DocumentUploadState());
_labelRepository.addListener(
this,
onChanged: (labels) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
));
},
);
}
Future<String?> upload( Future<String?> upload(
Uint8List bytes, { Uint8List bytes, {
@@ -44,7 +32,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
Iterable<int> tags = const [], Iterable<int> tags = const [],
DateTime? createdAt, DateTime? createdAt,
int? asn, int? asn,
void Function(double)? onProgressChanged,
}) async { }) async {
final taskId = await _documentApi.create( final taskId = await _documentApi.create(
bytes, bytes,
@@ -55,17 +42,15 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
tags: tags, tags: tags,
createdAt: createdAt, createdAt: createdAt,
asn: asn, asn: asn,
onProgressChanged: onProgressChanged, onProgressChanged: (progress) {
if (!isClosed) {
emit(state.copyWith(uploadProgress: progress));
}
},
); );
if (taskId != null) { if (taskId != null) {
_tasksNotifier.listenToTaskChanges(taskId); _tasksNotifier.listenToTaskChanges(taskId);
} }
return taskId; return taskId;
} }
@override
Future<void> close() async {
_labelRepository.removeListener(this);
return super.close();
}
} }

View File

@@ -1,33 +1,17 @@
part of 'document_upload_cubit.dart'; part of 'document_upload_cubit.dart';
@immutable @immutable
class DocumentUploadState extends Equatable { class DocumentUploadState {
final Map<int, Tag> tags; final double? uploadProgress;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
const DocumentUploadState({ const DocumentUploadState({
this.tags = const {}, this.uploadProgress,
this.correspondents = const {},
this.documentTypes = const {},
}); });
@override
List<Object> get props => [
tags,
correspondents,
documentTypes,
];
DocumentUploadState copyWith({ DocumentUploadState copyWith({
Map<int, Tag>? tags, double? uploadProgress,
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
}) { }) {
return DocumentUploadState( return DocumentUploadState(
tags: tags ?? this.tags, uploadProgress: uploadProgress ?? this.uploadProgress,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
); );
} }
} }

View File

@@ -6,28 +6,24 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart';
import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart'; import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart'; import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart'; import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
import 'package:provider/provider.dart';
class DocumentUploadResult { class DocumentUploadResult {
final bool success; final bool success;
@@ -62,7 +58,6 @@ class _DocumentUploadPreparationPageState
final GlobalKey<FormBuilderState> _formKey = GlobalKey(); final GlobalKey<FormBuilderState> _formKey = GlobalKey();
Map<String, String> _errors = {}; Map<String, String> _errors = {};
bool _isUploadLoading = false;
late bool _syncTitleAndFilename; late bool _syncTitleAndFilename;
bool _showDatePickerDeleteIcon = false; bool _showDatePickerDeleteIcon = false;
final _now = DateTime.now(); final _now = DateTime.now();
@@ -75,21 +70,32 @@ class _DocumentUploadPreparationPageState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final labels = context.watch<LabelRepository>().state;
extendBodyBehindAppBar: false, return BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
resizeToAvoidBottomInset: true, builder: (context, state) {
floatingActionButton: Visibility( return Scaffold(
visible: MediaQuery.of(context).viewInsets.bottom == 0, extendBodyBehindAppBar: false,
child: FloatingActionButton.extended( resizeToAvoidBottomInset: true,
heroTag: "fab_document_upload", floatingActionButton: Visibility(
onPressed: _onSubmit, visible: MediaQuery.of(context).viewInsets.bottom == 0,
label: Text(S.of(context)!.upload), child: FloatingActionButton.extended(
icon: const Icon(Icons.upload), heroTag: "fab_document_upload",
), onPressed: state.uploadProgress == null ? _onSubmit : null,
), label: state.uploadProgress == null
body: BlocBuilder<DocumentUploadCubit, DocumentUploadState>( ? Text(S.of(context)!.upload)
builder: (context, state) { : Text("Uploading..."), //TODO: INTL
return FormBuilder( icon: state.uploadProgress == null
? const Icon(Icons.upload)
: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
strokeWidth: 3,
value: state.uploadProgress,
)).padded(4),
),
),
body: FormBuilder(
key: _formKey, key: _formKey,
child: NestedScrollView( child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
@@ -97,7 +103,7 @@ class _DocumentUploadPreparationPageState
handle: handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context), NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( sliver: SliverAppBar(
leading: BackButton(), leading: const BackButton(),
pinned: true, pinned: true,
expandedHeight: 150, expandedHeight: 150,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
@@ -105,7 +111,7 @@ class _DocumentUploadPreparationPageState
future: widget.fileBytes, future: widget.fileBytes,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return SizedBox.shrink(); return const SizedBox.shrink();
} }
return FileThumbnail( return FileThumbnail(
bytes: snapshot.data!, bytes: snapshot.data!,
@@ -117,12 +123,6 @@ class _DocumentUploadPreparationPageState
title: Text(S.of(context)!.prepareDocument), title: Text(S.of(context)!.prepareDocument),
collapseMode: CollapseMode.pin, collapseMode: CollapseMode.pin,
), ),
bottom: _isUploadLoading
? PreferredSize(
child: LinearProgressIndicator(),
preferredSize: Size.fromHeight(4.0),
)
: null,
), ),
), ),
], ],
@@ -219,32 +219,13 @@ class _DocumentUploadPreparationPageState
), ),
), ),
// Created at // Created at
FormBuilderDateTimePicker( FormBuilderLocalizedDatePicker(
autovalidateMode: AutovalidateMode.always,
format: DateFormat.yMMMMd(
Localizations.localeOf(context).toString()),
inputType: InputType.date,
name: DocumentModel.createdKey, name: DocumentModel.createdKey,
initialValue: null, firstDate: DateTime(1970, 1, 1),
onChanged: (value) { lastDate: DateTime.now(),
setState(() => locale: Localizations.localeOf(context),
_showDatePickerDeleteIcon = value != null); labelText: S.of(context)!.createdAt + " *",
}, allowUnset: true,
decoration: InputDecoration(
prefixIcon:
const Icon(Icons.calendar_month_outlined),
labelText: S.of(context)!.createdAt + " *",
suffixIcon: _showDatePickerDeleteIcon
? IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_formKey.currentState!
.fields[DocumentModel.createdKey]
?.didChange(null);
},
)
: null,
),
), ),
// Correspondent // Correspondent
if (context if (context
@@ -261,7 +242,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addCorrespondent, addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *", labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey, name: DocumentModel.correspondentKey,
options: state.correspondents, options: labels.correspondents,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true, allowSelectUnassigned: true,
canCreateNewLabel: context canCreateNewLabel: context
@@ -284,7 +265,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addDocumentType, addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *", labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey, name: DocumentModel.documentTypeKey,
options: state.documentTypes, options: labels.documentTypes,
prefixIcon: prefixIcon:
const Icon(Icons.description_outlined), const Icon(Icons.description_outlined),
allowSelectUnassigned: true, allowSelectUnassigned: true,
@@ -302,7 +283,7 @@ class _DocumentUploadPreparationPageState
allowCreation: true, allowCreation: true,
allowExclude: false, allowExclude: false,
allowOnlySelection: true, allowOnlySelection: true,
options: state.tags, options: labels.tags,
), ),
Text( Text(
"* " + S.of(context)!.uploadInferValuesHint, "* " + S.of(context)!.uploadInferValuesHint,
@@ -318,9 +299,9 @@ class _DocumentUploadPreparationPageState
), ),
), ),
), ),
); ),
}, );
), },
); );
} }
@@ -328,7 +309,6 @@ class _DocumentUploadPreparationPageState
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final cubit = context.read<DocumentUploadCubit>(); final cubit = context.read<DocumentUploadCubit>();
try { try {
setState(() => _isUploadLoading = true);
final formValues = _formKey.currentState!.value; final formValues = _formKey.currentState!.value;
final correspondentParam = final correspondentParam =
@@ -336,7 +316,7 @@ class _DocumentUploadPreparationPageState
final docTypeParam = final docTypeParam =
formValues[DocumentModel.documentTypeKey] as IdQueryParameter?; formValues[DocumentModel.documentTypeKey] as IdQueryParameter?;
final tagsParam = formValues[DocumentModel.tagsKey] as TagsQuery?; final tagsParam = formValues[DocumentModel.tagsKey] as TagsQuery?;
final createdAt = formValues[DocumentModel.createdKey] as DateTime?; final createdAt = formValues[DocumentModel.createdKey] as FormDateTime?;
final title = formValues[DocumentModel.titleKey] as String; final title = formValues[DocumentModel.titleKey] as String;
final correspondent = switch (correspondentParam) { final correspondent = switch (correspondentParam) {
SetIdQueryParameter(id: var id) => id, SetIdQueryParameter(id: var id) => id,
@@ -365,7 +345,7 @@ class _DocumentUploadPreparationPageState
documentType: docType, documentType: docType,
correspondent: correspondent, correspondent: correspondent,
tags: tags, tags: tags,
createdAt: createdAt, createdAt: createdAt?.toDateTime(),
asn: asn, asn: asn,
); );
showSnackBar( showSnackBar(
@@ -390,10 +370,6 @@ class _DocumentUploadPreparationPageState
const PaperlessApiException.unknown(), const PaperlessApiException.unknown(),
stackTrace, stackTrace,
); );
} finally {
setState(() {
_isUploadLoading = false;
});
} }
} }
} }

View File

@@ -5,7 +5,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/extensions/document_iterable_extensions.dart'; import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';

View File

@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
@@ -404,7 +405,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
return SliverAdaptiveDocumentsView( return SliverAdaptiveDocumentsView(
viewType: state.viewType, viewType: state.viewType,
onTap: (document) { onTap: (document) {
DocumentDetailsRoute($extra: document).push(context); DocumentDetailsRoute(
title: document.title,
id: document.id,
thumbnailUrl: document.buildThumbnailUrl(context),
).push(context);
}, },
onSelected: onSelected:
context.read<DocumentsCubit>().toggleDocumentSelection, context.read<DocumentsCubit>().toggleDocumentSelection,

View File

@@ -9,7 +9,8 @@ import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
class DocumentPreview extends StatelessWidget { class DocumentPreview extends StatelessWidget {
final DocumentModel document; final int documentId;
final String? title;
final BoxFit fit; final BoxFit fit;
final Alignment alignment; final Alignment alignment;
final double borderRadius; final double borderRadius;
@@ -19,13 +20,14 @@ class DocumentPreview extends StatelessWidget {
const DocumentPreview({ const DocumentPreview({
super.key, super.key,
required this.document, required this.documentId,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.alignment = Alignment.topCenter, this.alignment = Alignment.topCenter,
this.borderRadius = 12.0, this.borderRadius = 12.0,
this.enableHero = true, this.enableHero = true,
this.scale = 1.1, this.scale = 1.1,
this.isClickable = true, this.isClickable = true,
this.title,
}); });
@override @override
@@ -34,12 +36,12 @@ class DocumentPreview extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: isClickable onTap: isClickable
? () => DocumentPreviewRoute($extra: document).push(context) ? () => DocumentPreviewRoute(id: documentId).push(context)
: null, : null,
child: Builder(builder: (context) { child: Builder(builder: (context) {
if (enableHero) { if (enableHero) {
return Hero( return Hero(
tag: "thumb_${document.id}", tag: "thumb_$documentId",
child: _buildPreview(context), child: _buildPreview(context),
); );
} }
@@ -57,10 +59,9 @@ class DocumentPreview extends StatelessWidget {
child: CachedNetworkImage( child: CachedNetworkImage(
fit: fit, fit: fit,
alignment: alignment, alignment: alignment,
cacheKey: "thumb_${document.id}", cacheKey: "thumb_$documentId",
imageUrl: context imageUrl:
.read<PaperlessDocumentsApi>() context.read<PaperlessDocumentsApi>().getThumbnailUrl(documentId),
.getThumbnailUrl(document.id),
errorWidget: (ctxt, msg, __) => Text(msg), errorWidget: (ctxt, msg, __) => Text(msg),
placeholder: (context, value) => Shimmer.fromColors( placeholder: (context, value) => Shimmer.fromColors(
baseColor: Colors.grey[300]!, baseColor: Colors.grey[300]!,

View File

@@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
@@ -56,6 +57,7 @@ class DocumentDetailedItem extends DocumentItem {
final maxHeight = highlights != null final maxHeight = highlights != null
? min(600.0, availableHeight) ? min(600.0, availableHeight)
: min(500.0, availableHeight); : min(500.0, availableHeight);
final labels = context.watch<LabelRepository>().state;
return Card( return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null, color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
child: InkWell( child: InkWell(
@@ -79,39 +81,71 @@ class DocumentDetailedItem extends DocumentItem {
width: double.infinity, width: double.infinity,
height: maxHeight / 2, height: maxHeight / 2,
), ),
child: DocumentPreview( child: Stack(
document: document, fit: StackFit.expand,
fit: BoxFit.cover, children: [
alignment: Alignment.topCenter, DocumentPreview(
documentId: document.id,
title: document.title,
),
if (paperlessUser.canViewTags)
Align(
alignment: Alignment.bottomLeft,
child: TagsWidget(
tags:
document.tags.map((e) => labels.tags[e]!).toList(),
onTagSelected: onTagSelected,
).padded(),
),
],
), ),
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Expanded(
DateFormat.yMMMMd(Localizations.localeOf(context).toString()) child: RichText(
.format(document.created), maxLines: 1,
style: Theme.of(context) overflow: TextOverflow.ellipsis,
.textTheme text: TextSpan(
.bodySmall style: Theme.of(context)
?.apply(color: Theme.of(context).hintColor), .textTheme
.bodySmall
?.apply(color: Theme.of(context).hintColor),
text: DateFormat.yMMMMd(
Localizations.localeOf(context).toString())
.format(document.created),
children: [
if (paperlessUser.canViewDocumentTypes &&
document.documentType != null) ...[
const TextSpan(text: '\u30FB'),
TextSpan(
text: labels
.documentTypes[document.documentType]?.name,
recognizer: onDocumentTypeSelected != null
? (TapGestureRecognizer()
..onTap = () => onDocumentTypeSelected!(
document.documentType))
: null,
),
],
],
),
),
), ),
if (document.archiveSerialNumber != null) if (document.archiveSerialNumber != null)
Row( Text(
children: [ '#${document.archiveSerialNumber}',
Text( style: Theme.of(context)
'#${document.archiveSerialNumber}', .textTheme
style: Theme.of(context) .bodySmall
.textTheme ?.apply(color: Theme.of(context).hintColor),
.bodySmall
?.apply(color: Theme.of(context).hintColor),
),
],
), ),
], ],
).paddedLTRB(8, 8, 8, 4), ).paddedLTRB(8, 8, 8, 4),
Text( Text(
document.title.isEmpty ? '-' : document.title, document.title.isEmpty ? '(-)' : document.title,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -128,39 +162,11 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply( textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
correspondent: context correspondent:
.watch<LabelRepository>() labels.correspondents[document.correspondent],
.state
.correspondents[document.correspondent],
), ),
], ],
).paddedLTRB(8, 0, 8, 4), ).paddedLTRB(8, 0, 8, 8),
if (paperlessUser.canViewDocumentTypes)
Row(
children: [
const Icon(
Icons.description_outlined,
size: 16,
).paddedOnly(right: 4.0),
DocumentTypeWidget(
onSelected: onDocumentTypeSelected,
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
documentType: context
.watch<LabelRepository>()
.state
.documentTypes[document.documentType],
),
],
).paddedLTRB(8, 0, 8, 4),
if (paperlessUser.canViewTags)
TagsWidget(
tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
.toList(),
onTagSelected: onTagSelected,
).padded(),
if (highlights != null) if (highlights != null)
Html( Html(
data: '<p>${highlights!}</p>', data: '<p>${highlights!}</p>',

View File

@@ -49,7 +49,7 @@ class DocumentGridItem extends DocumentItem {
children: [ children: [
Positioned.fill( Positioned.fill(
child: DocumentPreview( child: DocumentPreview(
document: document, documentId: document.id,
borderRadius: 12.0, borderRadius: 12.0,
enableHero: enableHeroAnimation, enableHero: enableHeroAnimation,
), ),

View File

@@ -75,31 +75,34 @@ class DocumentListItem extends DocumentItem {
), ),
], ],
), ),
subtitle: Padding( subtitle: IntrinsicWidth(
padding: const EdgeInsets.symmetric(vertical: 4), child: Padding(
child: RichText( padding: const EdgeInsets.symmetric(vertical: 4),
maxLines: 1, child: RichText(
overflow: TextOverflow.ellipsis, maxLines: 1,
text: TextSpan( overflow: TextOverflow.ellipsis,
text: DateFormat.yMMMMd(Localizations.localeOf(context).toString()) text: TextSpan(
.format(document.created), text:
style: Theme.of(context) DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.textTheme .format(document.created),
.labelSmall style: Theme.of(context)
?.apply(color: Colors.grey), .textTheme
children: document.documentType != null .labelSmall
? [ ?.apply(color: Colors.grey),
const TextSpan(text: '\u30FB'), children: document.documentType != null
TextSpan( ? [
text: labels.documentTypes[document.documentType]?.name, const TextSpan(text: '\u30FB'),
recognizer: onDocumentTypeSelected != null TextSpan(
? (TapGestureRecognizer() text: labels.documentTypes[document.documentType]?.name,
..onTap = () => recognizer: onDocumentTypeSelected != null
onDocumentTypeSelected!(document.documentType)) ? (TapGestureRecognizer()
: null, ..onTap = () => onDocumentTypeSelected!(
), document.documentType))
] : null,
: null, ),
]
: null,
),
), ),
), ),
), ),
@@ -108,7 +111,7 @@ class DocumentListItem extends DocumentItem {
aspectRatio: _a4AspectRatio, aspectRatio: _a4AspectRatio,
child: GestureDetector( child: GestureDetector(
child: DocumentPreview( child: DocumentPreview(
document: document, documentId: document.id,
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
enableHero: enableHeroAnimation, enableHero: enableHeroAnimation,

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
@@ -153,7 +154,9 @@ class _InboxItemState extends State<InboxItem> {
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () { onTap: () {
DocumentDetailsRoute( DocumentDetailsRoute(
$extra: widget.document, title: widget.document.title,
id: widget.document.id,
thumbnailUrl: widget.document.buildThumbnailUrl(context),
isLabelClickable: false, isLabelClickable: false,
).push(context); ).push(context);
}, },
@@ -168,7 +171,8 @@ class _InboxItemState extends State<InboxItem> {
AspectRatio( AspectRatio(
aspectRatio: InboxItem.a4AspectRatio, aspectRatio: InboxItem.a4AspectRatio,
child: DocumentPreview( child: DocumentPreview(
document: widget.document, documentId: widget.document.id,
title: widget.document.title,
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
enableHero: false, enableHero: false,

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
@@ -53,8 +54,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
onTap: (document) { onTap: (document) {
DocumentDetailsRoute( DocumentDetailsRoute(
$extra: document, title: document.title,
id: document.id,
isLabelClickable: false, isLabelClickable: false,
thumbnailUrl: document.buildThumbnailUrl(context),
).push(context); ).push(context);
}, },
), ),

View File

@@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/extensions/document_iterable_extensions.dart'; import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
@@ -55,8 +56,12 @@ class SavedViewPreview extends StatelessWidget {
isSelected: false, isSelected: false,
isSelectionActive: false, isSelectionActive: false,
onTap: (document) { onTap: (document) {
DocumentDetailsRoute($extra: document) DocumentDetailsRoute(
.push(context); title: document.title,
id: document.id,
thumbnailUrl:
document.buildThumbnailUrl(context),
).push(context);
}, },
onSelected: null, onSelected: null,
), ),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
@@ -66,7 +67,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
enableHeroAnimation: false, enableHeroAnimation: false,
onTap: (document) { onTap: (document) {
DocumentDetailsRoute( DocumentDetailsRoute(
$extra: document, title: document.title,
id: document.id,
thumbnailUrl: document.buildThumbnailUrl(context),
isLabelClickable: false, isLabelClickable: false,
).push(context); ).push(context);
}, },

View File

@@ -14,7 +14,6 @@ import 'package:paperless_mobile/features/documents/view/pages/document_view.dar
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/theme.dart';
class DocumentsBranch extends StatefulShellBranchData { class DocumentsBranch extends StatefulShellBranchData {
@@ -33,14 +32,18 @@ class DocumentDetailsRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = static final GlobalKey<NavigatorState> $parentNavigatorKey =
outerShellNavigatorKey; outerShellNavigatorKey;
final int id;
final bool isLabelClickable; final bool isLabelClickable;
final DocumentModel $extra;
final String? queryString; final String? queryString;
final String? thumbnailUrl;
final String? title;
const DocumentDetailsRoute({ const DocumentDetailsRoute({
required this.$extra, required this.id,
this.isLabelClickable = true, this.isLabelClickable = true,
this.queryString, this.queryString,
this.thumbnailUrl,
this.title,
}); });
@override @override
@@ -51,14 +54,15 @@ class DocumentDetailsRoute extends GoRouteData {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
initialDocument: $extra, id: id,
) )..initialize(),
..loadFullContent()
..loadMetaData(),
lazy: false, lazy: false,
child: DocumentDetailsPage( child: DocumentDetailsPage(
id: id,
isLabelClickable: isLabelClickable, isLabelClickable: isLabelClickable,
titleAndContentQueryString: queryString, titleAndContentQueryString: queryString,
thumbnailUrl: thumbnailUrl,
title: title,
), ),
); );
} }
@@ -96,20 +100,19 @@ class EditDocumentRoute extends GoRouteData {
class DocumentPreviewRoute extends GoRouteData { class DocumentPreviewRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey = static final GlobalKey<NavigatorState> $parentNavigatorKey =
outerShellNavigatorKey; outerShellNavigatorKey;
final int id;
final DocumentModel $extra;
final String? title; final String? title;
const DocumentPreviewRoute({ const DocumentPreviewRoute({
required this.$extra, required this.id,
this.title, this.title,
}); });
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return DocumentView( return DocumentView(
documentBytes: context.read<PaperlessDocumentsApi>().download($extra), documentBytes: context.read<PaperlessDocumentsApi>().downloadDocument(id),
title: title ?? $extra.title, title: title,
); );
} }
} }

View File

@@ -68,7 +68,7 @@ part 'authenticated_route.g.dart';
path: "/documents", path: "/documents",
routes: [ routes: [
TypedGoRoute<DocumentDetailsRoute>( TypedGoRoute<DocumentDetailsRoute>(
path: "details", path: "details/:id",
name: R.documentDetails, name: R.documentDetails,
), ),
TypedGoRoute<EditDocumentRoute>( TypedGoRoute<EditDocumentRoute>(

View File

@@ -21,11 +21,11 @@ abstract class PaperlessDocumentsApi {
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter); Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
Future<DocumentModel> find(int id); Future<DocumentModel> find(int id);
Future<int> delete(DocumentModel doc); Future<int> delete(DocumentModel doc);
Future<DocumentMetaData> getMetaData(DocumentModel document); Future<DocumentMetaData> getMetaData(int id);
Future<Iterable<int>> bulkAction(BulkAction action); Future<Iterable<int>> bulkAction(BulkAction action);
Future<Uint8List> getPreview(int docId); Future<Uint8List> getPreview(int docId);
String getThumbnailUrl(int docId); String getThumbnailUrl(int docId);
Future<Uint8List> download(DocumentModel document, {bool original}); Future<Uint8List> downloadDocument(int id, {bool original});
Future<void> downloadToFile( Future<void> downloadToFile(
DocumentModel document, DocumentModel document,
String localFilePath, { String localFilePath, {

View File

@@ -200,13 +200,13 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
@override @override
Future<Uint8List> download( Future<Uint8List> downloadDocument(
DocumentModel document, { int id, {
bool original = false, bool original = false,
}) async { }) async {
try { try {
final response = await client.get( final response = await client.get(
"/api/documents/${document.id}/download/", "/api/documents/$id/download/",
queryParameters: {'original': original}, queryParameters: {'original': original},
options: Options(responseType: ResponseType.bytes), options: Options(responseType: ResponseType.bytes),
); );
@@ -242,14 +242,20 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
} }
@override @override
Future<DocumentMetaData> getMetaData(DocumentModel document) async { Future<DocumentMetaData> getMetaData(int id) async {
debugPrint("Fetching data for /api/documents/$id/metadata/...");
try { try {
final response = final response = await client.get(
await client.get("/api/documents/${document.id}/metadata/"); "/api/documents/$id/metadata/",
return compute( options: Options(
DocumentMetaData.fromJson, sendTimeout: Duration(seconds: 10),
response.data as Map<String, dynamic>, receiveTimeout: Duration(seconds: 10),
),
); );
debugPrint("Fetched data for /api/documents/$id/metadata/.");
return DocumentMetaData.fromJson(response.data);
} on DioException catch (exception) { } on DioException catch (exception) {
throw exception.unravel( throw exception.unravel(
orElse: const PaperlessApiException.unknown(), orElse: const PaperlessApiException.unknown(),
@@ -296,11 +302,17 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override @override
Future<DocumentModel> find(int id) async { Future<DocumentModel> find(int id) async {
debugPrint("Fetching data from /api/documents/$id/...");
try { try {
final response = await client.get( final response = await client.get(
"/api/documents/$id/", "/api/documents/$id/",
options: Options(validateStatus: (status) => status == 200), options: Options(
validateStatus: (status) => status == 200,
sendTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
),
); );
debugPrint("Fetched data for /api/documents/$id/.");
return DocumentModel.fromJson(response.data); return DocumentModel.fromJson(response.data);
} on DioException catch (exception) { } on DioException catch (exception) {
throw exception.unravel( throw exception.unravel(