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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
extension DocumentModelIterableExtension on Iterable<DocumentModel> {
@@ -16,3 +18,8 @@ extension DocumentModelIterableExtension on Iterable<DocumentModel> {
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 'package:flutter/widgets.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
@@ -11,14 +10,12 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
LabelRepository(this._api) : super(const LabelRepositoryState());
Future<void> initialize() async {
await Future.wait([
findAllCorrespondents(),
findAllDocumentTypes(),
findAllStoragePaths(),
findAllTags(),
]);
await Future.wait([
findAllCorrespondents(),
findAllDocumentTypes(),
findAllStoragePaths(),
findAllTags(),
]);
}
Future<Tag> createTag(Tag object) async {

View File

@@ -1,3 +1,5 @@
// ignore_for_file: invalid_use_of_protected_member
import 'dart:collection';
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/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.
class FormBuilderLocalizedDatePicker extends StatefulWidget {
final String name;
@@ -124,42 +159,35 @@ class _FormBuilderLocalizedDatePickerState
}
}
},
child: FormBuilderField<DateTime>(
child: FormBuilderField<FormDateTime>(
name: widget.name,
validator: _validateDate,
onChanged: (value) {
// We have to temporarily disable our listeners on the TextEditingController here
// since otherwise the listeners get notified of the change and
// the fields get focused and highlighted/selected (as defined in the
// listeners above).
_temporarilyDisableListeners = true;
for (var control in _textFieldControls) {
control.controller.text = DateFormat(control.format).format(value!);
assert(!widget.allowUnset && value != null);
if (value == null) {
return;
}
_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);
setState(() {
_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,
name: widget.name,
initialValue: widget.initialValue,
initialValue: widget.initialValue != null
? FormDateTime.fromDateTime(widget.initialValue!)
: null,
builder: (field) {
return GestureDetector(
onTap: () {
@@ -170,7 +198,6 @@ class _FormBuilderLocalizedDatePickerState
decoration: InputDecoration(
errorText: _error,
labelText: widget.labelText,
prefixIcon: widget.prefixIcon,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -179,28 +206,33 @@ class _FormBuilderLocalizedDatePickerState
onPressed: () async {
final selectedDate = await showDatePicker(
context: context,
initialDate: widget.initialValue ?? DateTime.now(),
initialDate:
field.value?.toDateTime() ?? DateTime.now(),
firstDate: widget.firstDate,
lastDate: widget.lastDate,
initialEntryMode: DatePickerEntryMode.calendarOnly,
);
if (selectedDate != null) {
_updateInputsWithDate(selectedDate);
field.didChange(selectedDate);
FocusScope.of(context).unfocus();
final formDate =
FormDateTime.fromDateTime(selectedDate);
_temporarilyDisableListeners = true;
_updateInputsWithDate(formDate);
field.didChange(formDate);
_temporarilyDisableListeners = false;
}
},
),
IconButton(
onPressed: () {
for (var c in _textFieldControls) {
c.controller.clear();
}
_textFieldControls.first.node.requestFocus();
field.didChange(null);
},
icon: const Icon(Icons.clear),
),
if (widget.allowUnset)
IconButton(
onPressed: () {
for (var c in _textFieldControls) {
c.controller.clear();
}
_textFieldControls.first.node.requestFocus();
field.didChange(null);
},
icon: const Icon(Icons.clear),
),
],
).paddedOnly(right: 4),
),
@@ -220,19 +252,26 @@ class _FormBuilderLocalizedDatePickerState
);
}
String? _validateDate(DateTime? date) {
String? _validateDate(FormDateTime? date) {
if (widget.allowUnset && date == null) {
return null;
}
if (date == null) {
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 =
DateFormat.yMd(widget.locale.toString()).format(widget.firstDate);
return "Date must be after $formattedDateHint.";
}
if (date.isAfter(widget.lastDate)) {
if (d.isAfter(widget.lastDate)) {
final formattedDateHint =
DateFormat.yMd(widget.locale.toString()).format(widget.lastDate);
return "Date must be before $formattedDateHint.";
@@ -240,30 +279,31 @@ class _FormBuilderLocalizedDatePickerState
return null;
}
void _updateInputsWithDate(DateTime date) {
final components = _format.split(_separator);
for (int i = 0; i < components.length; i++) {
final formatString = components[i];
final value = DateFormat(formatString).format(date);
_textFieldControls.elementAt(i).controller.text = value;
void _updateInputsWithDate(
FormDateTime date, {
bool disableListeners = false,
}) {
if (disableListeners) {
_temporarilyDisableListeners = true;
}
for (var controls in _textFieldControls) {
final value = DateFormat(controls.format).format(date.toDateTime()!);
controls.controller.text = value;
}
_temporarilyDisableListeners = false;
}
Widget _buildDateSegmentInput(
_NeighbourAwareDateInputSegmentControls controls,
BuildContext context,
FormFieldState<DateTime> field,
FormFieldState<FormDateTime> field,
) {
return TextFormField(
onFieldSubmitted: (value) {
if (value.length < controls.format.length) {
controls.controller.text = value.padLeft(controls.format.length, '0');
}
_textFieldControls
.elementAt(controls.position)
.next
?.node
.requestFocus();
controls.next?.node.requestFocus();
},
style: const TextStyle(fontFamily: 'RobotoMono'),
keyboardType: TextInputType.datetime,
@@ -275,17 +315,18 @@ class _FormBuilderLocalizedDatePickerState
maxLengthEnforcement: MaxLengthEnforcement.enforced,
enableInteractiveSelection: false,
onChanged: (value) {
if (value.length == controls.format.length && field.value != null) {
if (value.length == controls.format.length) {
final number = int.tryParse(value);
if (number == null) {
return;
}
final fieldValue = field.value ?? FormDateTime();
final newValue = switch (controls.type) {
_DateInputSegment.day => field.value!.copyWith(day: number),
_DateInputSegment.month => field.value!.copyWith(month: number),
_DateInputSegment.year => field.value!.copyWith(year: number),
_DateInputSegment.day => fieldValue.copyWith(day: number),
_DateInputSegment.month => fieldValue.copyWith(month: number),
_DateInputSegment.year => fieldValue.copyWith(year: number),
};
field.didChange(newValue);
field.setValue(newValue);
}
},
inputFormatters: [
@@ -299,6 +340,12 @@ class _FormBuilderLocalizedDatePickerState
},
),
],
onEditingComplete: () {
if (field.value != null) {
_updateInputsWithDate(field.value!, disableListeners: true);
}
FocusScope.of(context).unfocus();
},
decoration: InputDecoration(
isDense: true,
suffixIcon: controls.position < 2

View File

@@ -2,23 +2,23 @@ import 'dart:async';
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/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:path/path.dart' as p;
import 'package:printing/printing.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';
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final int id;
final PaperlessDocumentsApi _api;
final DocumentChangedNotifier _notifier;
final LocalNotificationService _notificationService;
@@ -29,24 +29,46 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
this._labelRepository,
this._notifier,
this._notificationService, {
required DocumentModel initialDocument,
}) : super(DocumentDetailsState(document: initialDocument)) {
required this.id,
}) : super(const DocumentDetailsInitial()) {
_notifier.addListener(this, onUpdated: (document) {
if (document.id == state.document.id) {
replace(document);
if (state is DocumentDetailsLoaded) {
final currentState = state as DocumentDetailsLoaded;
if (document.id == currentState.document.id) {
replace(document);
}
}
});
_labelRepository.addListener(
this,
onChanged: (labels) => emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
),
),
);
}
Future<void> initialize() async {
debugPrint("Initialize called");
emit(const DocumentDetailsLoading());
try {
final (document, metaData) = await Future.wait([
_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 {
@@ -54,20 +76,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
_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(
DocumentModel document, {
int? asn,
@@ -87,11 +95,15 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
}
Future<ResultType> openDocumentInSystemViewer() async {
final cacheDir = FileService.instance.temporaryDirectory;
if (state.metaData == null) {
await loadMetaData();
final s = state;
if (s is! DocumentDetailsLoaded) {
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 file = File("${cacheDir.path}/$fileName");
@@ -99,7 +111,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (!file.existsSync()) {
file.createSync();
await _api.downloadToFile(
state.document,
s.document,
file.path,
);
}
@@ -110,7 +122,14 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
}
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({
@@ -118,10 +137,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
required String locale,
required String userId,
}) async {
if (state.metaData == null) {
await loadMetaData();
final s = state;
if (s is! DocumentDetailsLoaded) {
return;
}
String targetPath = _buildDownloadFilePath(
s.metaData,
downloadOriginal,
FileService.instance.downloadsDirectory,
);
@@ -130,7 +151,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
await File(targetPath).create();
} else {
await _notificationService.notifyDocumentDownload(
document: state.document,
document: s.document,
filename: p.basename(targetPath),
filePath: targetPath,
finished: true,
@@ -149,12 +170,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
// );
await _api.downloadToFile(
state.document,
s.document,
targetPath,
original: downloadOriginal,
onProgressChanged: (progress) {
_notificationService.notifyDocumentDownload(
document: state.document,
document: s.document,
filename: p.basename(targetPath),
filePath: targetPath,
finished: true,
@@ -165,26 +186,28 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
},
);
await _notificationService.notifyDocumentDownload(
document: state.document,
document: s.document,
filename: p.basename(targetPath),
filePath: targetPath,
finished: true,
locale: locale,
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 {
if (state.metaData == null) {
await loadMetaData();
final s = state;
if (s is! DocumentDetailsLoaded) {
return;
}
String filePath = _buildDownloadFilePath(
s.metaData,
shareOriginal,
FileService.instance.temporaryDirectory,
);
await _api.downloadToFile(
state.document,
s.document,
filePath,
original: shareOriginal,
);
@@ -192,23 +215,27 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
[
XFile(
filePath,
name: state.document.originalFileName,
name: s.document.originalFileName,
mimeType: "application/pdf",
lastModified: state.document.modified,
lastModified: s.document.modified,
),
],
subject: state.document.title,
subject: s.document.title,
);
}
Future<void> printDocument() async {
if (state.metaData == null) {
await loadMetaData();
final s = state;
if (s is! DocumentDetailsLoaded) {
return;
}
final filePath =
_buildDownloadFilePath(false, FileService.instance.temporaryDirectory);
final filePath = _buildDownloadFilePath(
s.metaData,
false,
FileService.instance.temporaryDirectory,
);
await _api.downloadToFile(
state.document,
s.document,
filePath,
original: false,
);
@@ -217,13 +244,14 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
throw Exception("An error occurred while downloading the document.");
}
Printing.layoutPdf(
name: state.document.title,
name: s.document.title,
onLayout: (format) => file.readAsBytesSync(),
);
}
String _buildDownloadFilePath(bool original, Directory dir) {
final normalizedPath = state.metaData!.mediaFilename.replaceAll("/", " ");
String _buildDownloadFilePath(
DocumentMetaData meta, bool original, Directory dir) {
final normalizedPath = meta.mediaFilename.replaceAll("/", " ");
final extension = original ? p.extension(normalizedPath) : '.pdf';
return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension";
}

View File

@@ -1,14 +1,41 @@
part of 'document_details_cubit.dart';
@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;
sealed class DocumentDetailsState {
const 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:go_router/go_router.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.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/extensions/flutter_extensions.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/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/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_meta_data_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
@@ -29,13 +27,21 @@ import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
import 'package:paperless_mobile/theme.dart';
class DocumentDetailsPage extends StatefulWidget {
final int id;
final String? title;
final bool isLabelClickable;
final String? titleAndContentQueryString;
final String? thumbnailUrl;
final String? heroTag;
const DocumentDetailsPage({
Key? key,
this.isLabelClickable = true,
this.titleAndContentQueryString,
this.thumbnailUrl,
required this.id,
this.heroTag,
this.title,
}) : super(key: key);
@override
@@ -57,152 +63,157 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final hasMultiUserSupport =
context.watch<LocalUserAccount>().hasMultiUserSupport;
final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0);
final title = context.watch<DocumentDetailsCubit>().state.document.title;
return AnnotatedRegion(
value: buildOverlayStyle(
Theme.of(context),
systemNavigationBarColor: Theme.of(context).bottomAppBarTheme.color,
),
child: WillPopScope(
onWillPop: () async {
Navigator.of(context)
.pop(context.read<DocumentDetailsCubit>().state.document);
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: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
return DefaultTabController(
length: tabLength,
child: Scaffold(
extendBodyBehindAppBar: false,
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
floatingActionButton: _buildEditButton(),
floatingActionButton: switch (state) {
DocumentDetailsLoaded(document: var document) =>
_buildEditButton(document),
_ => null
},
bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
sliver: SliverAppBar(
title: Text(title),
leading: const BackButton(),
pinned: true,
forceElevated: innerBoxIsScrolled,
collapsedHeight: kToolbarHeight,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
background: BlocBuilder<DocumentDetailsCubit,
DocumentDetailsState>(
builder: (context, state) {
return Hero(
tag: "thumb_${state.document.id}",
child: GestureDetector(
onTap: () {
DocumentPreviewRoute($extra: state.document)
.push(context);
},
child: Stack(
alignment: Alignment.topCenter,
children: [
Positioned.fill(
child: DocumentPreview(
enableHero: false,
document: state.document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
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,
sliver:
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
final title = switch (state) {
DocumentDetailsLoaded(document: var document) =>
document.title,
_ => widget.title ?? '',
};
return SliverAppBar(
title: Text(title),
leading: const BackButton(),
pinned: true,
forceElevated: innerBoxIsScrolled,
collapsedHeight: kToolbarHeight,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
background: Builder(
builder: (context) {
return Hero(
tag: widget.heroTag ?? "thumb_${widget.id}",
child: GestureDetector(
onTap: () {
DocumentPreviewRoute(
id: widget.id,
title: title,
).push(context);
},
child: Stack(
alignment: Alignment.topCenter,
children: [
Positioned.fill(
child: DocumentPreview(
documentId: widget.id,
title: title,
enableHero: false,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
),
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(
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,
),
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(
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(),
documentId: state.document.id,
documentId: widget.id,
),
child: Padding(
padding: const EdgeInsets.symmetric(
@@ -229,12 +240,19 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentOverviewWidget(
document: state.document,
itemSpacing: _itemSpacing,
queryString:
widget.titleAndContentQueryString,
),
switch (state) {
DocumentDetailsLoaded(
document: var document
) =>
DocumentOverviewWidget(
document: document,
itemSpacing: _itemSpacing,
queryString:
widget.titleAndContentQueryString,
),
DocumentDetailsError() => _buildErrorState(),
_ => _buildLoadingState(),
},
],
),
CustomScrollView(
@@ -243,13 +261,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentContentWidget(
isFullContentLoaded:
state.isFullContentLoaded,
document: state.document,
queryString:
widget.titleAndContentQueryString,
),
switch (state) {
DocumentDetailsLoaded(
document: var document
) =>
DocumentContentWidget(
document: document,
queryString:
widget.titleAndContentQueryString,
),
DocumentDetailsError() => _buildErrorState(),
_ => _buildLoadingState(),
}
],
),
CustomScrollView(
@@ -258,10 +281,19 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentMetaDataWidget(
document: state.document,
itemSpacing: _itemSpacing,
),
switch (state) {
DocumentDetailsLoaded(
document: var document,
metaData: var metaData,
) =>
DocumentMetaDataWidget(
document: document,
itemSpacing: _itemSpacing,
metaData: metaData,
),
DocumentDetailsError() => _buildErrorState(),
_ => _buildLoadingState(),
},
],
),
CustomScrollView(
@@ -277,20 +309,20 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
],
),
if (hasMultiUserSupport && false)
CustomScrollView(
controller: _pagingScrollController,
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(
context),
),
DocumentPermissionsWidget(
document: state.document,
),
],
),
// if (hasMultiUserSupport && false)
// CustomScrollView(
// controller: _pagingScrollController,
// slivers: [
// SliverOverlapInjector(
// handle: NestedScrollView
// .sliverOverlapAbsorberHandleFor(
// context),
// ),
// DocumentPermissionsWidget(
// document: state.document,
// ),
// ],
// ),
],
),
),
@@ -299,13 +331,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
),
),
),
),
);
},
),
);
}
Widget _buildEditButton() {
Widget _buildEditButton(DocumentModel document) {
final currentUser = context.watch<LocalUserAccount>();
bool canEdit = context.watchInternetConnection &&
@@ -313,7 +345,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
if (!canEdit) {
return const SizedBox.shrink();
}
final document = context.read<DocumentDetailsCubit>().state.document;
return Tooltip(
message: S.of(context)!.editDocumentTooltip,
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() {
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
final currentUser = context.watch<LocalUserAccount>();
return BottomAppBar(
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
final currentUser = context.watch<LocalUserAccount>();
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ConnectivityAwareActionWrapper(
disabled: !currentUser.paperlessUser.canDeleteDocuments,
offlineBuilder: (context, child) {
return const IconButton(
icon: Icon(Icons.delete),
onPressed: null,
).paddedSymmetrically(horizontal: 4);
},
child: IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed: () => _onDelete(state.document),
).paddedSymmetrically(horizontal: 4),
child: Builder(
builder: (context) {
return switch (state) {
DocumentDetailsLoaded(document: var document) => Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ConnectivityAwareActionWrapper(
disabled: !currentUser.paperlessUser.canDeleteDocuments,
offlineBuilder: (context, child) {
return const IconButton(
icon: Icon(Icons.delete),
onPressed: null,
).paddedSymmetrically(horizontal: 4);
},
child: IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed: () => _onDelete(document),
).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(
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),
),
],
);
_ => SizedBox.shrink(),
};
},
),
);
@@ -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;
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) =>
previous is DocumentDetailsLoaded &&
current is DocumentDetailsLoaded &&
previous.document.archiveSerialNumber !=
current.document.archiveSerialNumber,
current.document.archiveSerialNumber,
listener: (context, state) {
_asnEditingController.text =
state.document.archiveSerialNumber?.toString() ?? '';
_asnEditingController.text = (state as DocumentDetailsLoaded)
.document
.archiveSerialNumber
?.toString() ??
'';
setState(() {
_canUpdate = false;
});

View File

@@ -1,26 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.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 {
final bool isFullContentLoaded;
final String? queryString;
final DocumentModel document;
final String? queryString;
const DocumentContentWidget({
super.key,
required this.isFullContentLoaded,
required this.document,
this.queryString,
});
@override
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(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -31,21 +42,6 @@ class DocumentContentWidget extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium,
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_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
class DocumentMetaDataWidget extends StatefulWidget {
class DocumentMetaDataWidget extends StatelessWidget {
final DocumentModel document;
final DocumentMetaData metaData;
final double itemSpacing;
const DocumentMetaDataWidget({
super.key,
required this.document,
required this.metaData,
required this.itemSpacing,
});
@override
State<DocumentMetaDataWidget> createState() => _DocumentMetaDataWidgetState();
}
class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
@override
Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
if (state.metaData == null) {
return const SliverToBoxAdapter(
child: Center(
child: CircularProgressIndicator(),
),
);
}
return SliverList(
delegate: SliverChildListDelegate(
[
if (currentUser.canEditDocuments)
ArchiveSerialNumberField(
document: widget.document,
).paddedOnly(bottom: widget.itemSpacing),
DetailsItem.text(
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(widget.document.modified),
context: context,
label: S.of(context)!.modifiedAt,
).paddedOnly(bottom: widget.itemSpacing),
DetailsItem.text(
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(widget.document.added),
context: context,
label: S.of(context)!.addedAt,
).paddedOnly(bottom: widget.itemSpacing),
DetailsItem.text(
state.metaData!.mediaFilename,
context: context,
label: S.of(context)!.mediaFilename,
).paddedOnly(bottom: widget.itemSpacing),
if (state.document.originalFileName != null)
DetailsItem.text(
state.document.originalFileName!,
context: context,
label: S.of(context)!.originalMD5Checksum,
).paddedOnly(bottom: widget.itemSpacing),
DetailsItem.text(
state.metaData!.originalChecksum,
context: context,
label: S.of(context)!.originalMD5Checksum,
).paddedOnly(bottom: widget.itemSpacing),
DetailsItem.text(
formatBytes(state.metaData!.originalSize, 2),
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),
],
),
);
},
return SliverList(
delegate: SliverChildListDelegate(
[
if (currentUser.canEditDocuments)
ArchiveSerialNumberField(
document: document,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(document.modified),
context: context,
label: S.of(context)!.modifiedAt,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(document.added),
context: context,
label: S.of(context)!.addedAt,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
metaData.mediaFilename,
context: context,
label: S.of(context)!.mediaFilename,
).paddedOnly(bottom: itemSpacing),
if (document.originalFileName != null)
DetailsItem.text(
document.originalFileName!,
context: context,
label: S.of(context)!.originalMD5Checksum,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
metaData.originalChecksum,
context: context,
label: S.of(context)!.originalMD5Checksum,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
formatBytes(metaData.originalSize, 2),
context: context,
label: S.of(context)!.originalFileSize,
).paddedOnly(bottom: itemSpacing),
DetailsItem.text(
metaData.originalMimeType,
context: context,
label: S.of(context)!.originalMIMEType,
).paddedOnly(bottom: 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/widgets/highlighted_text.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/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
@@ -27,6 +28,7 @@ class DocumentOverviewWidget extends StatelessWidget {
Widget build(BuildContext context) {
final user = context.watch<LocalUserAccount>().paperlessUser;
final availableLabels = context.watch<LabelRepository>().state;
return SliverList.list(
children: [
if (document.title.isNotEmpty)

View File

@@ -27,22 +27,6 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
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 {
@@ -76,7 +60,6 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
@override
Future<void> close() {
_notifier.removeListener(this);
_labelRepository.removeListener(this);
return super.close();
}
}

View File

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

View File

@@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.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/form_builder_fields/form_builder_localized_date_picker.dart';
@@ -45,39 +46,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override
Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
return BlocConsumer<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);
}
},
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
builder: (context, state) {
final filteredSuggestions = state.suggestions;
return PopWithUnsavedChanges(
@@ -160,7 +129,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options: context
.watch<DocumentEditCubit>()
.watch<LabelRepository>()
.state
.correspondents,
initialValue: state
@@ -203,7 +172,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
? SetIdQueryParameter(
id: state.document.documentType!)
: const UnsetIdQueryParameter(),
options: state.documentTypes,
options: context
.watch<LabelRepository>()
.state
.documentTypes,
name: _DocumentEditPageState.fkDocumentType,
prefixIcon:
const Icon(Icons.description_outlined),
@@ -230,7 +202,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
currentUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
options: context
.watch<LabelRepository>()
.state
.storagePaths,
initialValue:
state.document.storagePath != null
? SetIdQueryParameter(
@@ -246,7 +221,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
// Tag form field
if (currentUser.canViewTags)
TagsFormField(
options: state.tags,
options:
context.watch<LabelRepository>().state.tags,
name: fkTags,
allowOnlySelection: 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,
int? correspondent,
@@ -333,7 +285,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
fkState.getRawValue<IdQueryParameter?>(fkStoragePath);
final tagsParam = fkState.getRawValue<TagsQuery?>(fkTags);
final title = fkState.getRawValue<String?>(fkTitle);
final created = fkState.getRawValue<DateTime?>(fkCreatedDate);
final created = fkState.getRawValue<FormDateTime?>(fkCreatedDate);
final correspondent = switch (correspondentParam) {
SetIdQueryParameter(id: var id) => id,
_ => null,
@@ -358,7 +310,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
documentType,
storagePath,
tags,
created,
created?.toDateTime(),
content
);
}
@@ -432,7 +384,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(itemData)),
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:flutter/material.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/features/document_search/cubit/document_search_cubit.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,
enableHeroAnimation: false,
onTap: (document) {
DocumentDetailsRoute($extra: document, isLabelClickable: false)
.push(context);
DocumentDetailsRoute(
title: document.title,
id: document.id,
isLabelClickable: false,
thumbnailUrl: document.buildThumbnailUrl(context),
).push(context);
},
)
],

View File

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

View File

@@ -6,28 +6,24 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:go_router/go_router.dart';
import 'package:hive/hive.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/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/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/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/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/generated/l10n/app_localizations.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/shells/authenticated_route.dart';
import 'package:provider/provider.dart';
class DocumentUploadResult {
final bool success;
@@ -62,7 +58,6 @@ class _DocumentUploadPreparationPageState
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
Map<String, String> _errors = {};
bool _isUploadLoading = false;
late bool _syncTitleAndFilename;
bool _showDatePickerDeleteIcon = false;
final _now = DateTime.now();
@@ -75,21 +70,32 @@ class _DocumentUploadPreparationPageState
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: false,
resizeToAvoidBottomInset: true,
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
heroTag: "fab_document_upload",
onPressed: _onSubmit,
label: Text(S.of(context)!.upload),
icon: const Icon(Icons.upload),
),
),
body: BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, state) {
return FormBuilder(
final labels = context.watch<LabelRepository>().state;
return BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, state) {
return Scaffold(
extendBodyBehindAppBar: false,
resizeToAvoidBottomInset: true,
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
heroTag: "fab_document_upload",
onPressed: state.uploadProgress == null ? _onSubmit : null,
label: state.uploadProgress == null
? Text(S.of(context)!.upload)
: Text("Uploading..."), //TODO: INTL
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,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
@@ -97,7 +103,7 @@ class _DocumentUploadPreparationPageState
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
leading: BackButton(),
leading: const BackButton(),
pinned: true,
expandedHeight: 150,
flexibleSpace: FlexibleSpaceBar(
@@ -105,7 +111,7 @@ class _DocumentUploadPreparationPageState
future: widget.fileBytes,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox.shrink();
return const SizedBox.shrink();
}
return FileThumbnail(
bytes: snapshot.data!,
@@ -117,12 +123,6 @@ class _DocumentUploadPreparationPageState
title: Text(S.of(context)!.prepareDocument),
collapseMode: CollapseMode.pin,
),
bottom: _isUploadLoading
? PreferredSize(
child: LinearProgressIndicator(),
preferredSize: Size.fromHeight(4.0),
)
: null,
),
),
],
@@ -219,32 +219,13 @@ class _DocumentUploadPreparationPageState
),
),
// Created at
FormBuilderDateTimePicker(
autovalidateMode: AutovalidateMode.always,
format: DateFormat.yMMMMd(
Localizations.localeOf(context).toString()),
inputType: InputType.date,
FormBuilderLocalizedDatePicker(
name: DocumentModel.createdKey,
initialValue: null,
onChanged: (value) {
setState(() =>
_showDatePickerDeleteIcon = value != null);
},
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,
),
firstDate: DateTime(1970, 1, 1),
lastDate: DateTime.now(),
locale: Localizations.localeOf(context),
labelText: S.of(context)!.createdAt + " *",
allowUnset: true,
),
// Correspondent
if (context
@@ -261,7 +242,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: state.correspondents,
options: labels.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: context
@@ -284,7 +265,7 @@ class _DocumentUploadPreparationPageState
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey,
options: state.documentTypes,
options: labels.documentTypes,
prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
@@ -302,7 +283,7 @@ class _DocumentUploadPreparationPageState
allowCreation: true,
allowExclude: false,
allowOnlySelection: true,
options: state.tags,
options: labels.tags,
),
Text(
"* " + S.of(context)!.uploadInferValuesHint,
@@ -318,9 +299,9 @@ class _DocumentUploadPreparationPageState
),
),
),
);
},
),
),
);
},
);
}
@@ -328,7 +309,6 @@ class _DocumentUploadPreparationPageState
if (_formKey.currentState?.saveAndValidate() ?? false) {
final cubit = context.read<DocumentUploadCubit>();
try {
setState(() => _isUploadLoading = true);
final formValues = _formKey.currentState!.value;
final correspondentParam =
@@ -336,7 +316,7 @@ class _DocumentUploadPreparationPageState
final docTypeParam =
formValues[DocumentModel.documentTypeKey] as IdQueryParameter?;
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 correspondent = switch (correspondentParam) {
SetIdQueryParameter(id: var id) => id,
@@ -365,7 +345,7 @@ class _DocumentUploadPreparationPageState
documentType: docType,
correspondent: correspondent,
tags: tags,
createdAt: createdAt,
createdAt: createdAt?.toDateTime(),
asn: asn,
);
showSnackBar(
@@ -390,10 +370,6 @@ class _DocumentUploadPreparationPageState
const PaperlessApiException.unknown(),
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:paperless_api/paperless_api.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/repository/label_repository.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_mobile/core/bloc/connectivity_cubit.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/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
@@ -404,7 +405,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
return SliverAdaptiveDocumentsView(
viewType: state.viewType,
onTap: (document) {
DocumentDetailsRoute($extra: document).push(context);
DocumentDetailsRoute(
title: document.title,
id: document.id,
thumbnailUrl: document.buildThumbnailUrl(context),
).push(context);
},
onSelected:
context.read<DocumentsCubit>().toggleDocumentSelection,

View File

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

View File

@@ -1,5 +1,6 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:hive_flutter/adapters.dart';
@@ -56,6 +57,7 @@ class DocumentDetailedItem extends DocumentItem {
final maxHeight = highlights != null
? min(600.0, availableHeight)
: min(500.0, availableHeight);
final labels = context.watch<LabelRepository>().state;
return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
child: InkWell(
@@ -79,39 +81,71 @@ class DocumentDetailedItem extends DocumentItem {
width: double.infinity,
height: maxHeight / 2,
),
child: DocumentPreview(
document: document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
child: Stack(
fit: StackFit.expand,
children: [
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
.format(document.created),
style: Theme.of(context)
.textTheme
.bodySmall
?.apply(color: Theme.of(context).hintColor),
Expanded(
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: Theme.of(context)
.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)
Row(
children: [
Text(
'#${document.archiveSerialNumber}',
style: Theme.of(context)
.textTheme
.bodySmall
?.apply(color: Theme.of(context).hintColor),
),
],
Text(
'#${document.archiveSerialNumber}',
style: Theme.of(context)
.textTheme
.bodySmall
?.apply(color: Theme.of(context).hintColor),
),
],
).paddedLTRB(8, 8, 8, 4),
Text(
document.title.isEmpty ? '-' : document.title,
document.title.isEmpty ? '(-)' : document.title,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -128,39 +162,11 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
correspondent:
labels.correspondents[document.correspondent],
),
],
).paddedLTRB(8, 0, 8, 4),
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(),
).paddedLTRB(8, 0, 8, 8),
if (highlights != null)
Html(
data: '<p>${highlights!}</p>',

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/selection/view_type_selection_widget.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,
onTap: (document) {
DocumentDetailsRoute(
$extra: document,
title: document.title,
id: document.id,
isLabelClickable: false,
thumbnailUrl: document.buildThumbnailUrl(context),
).push(context);
},
),

View File

@@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:collection/collection.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/service/connectivity_status_service.dart';

View File

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

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
@@ -66,7 +67,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
enableHeroAnimation: false,
onTap: (document) {
DocumentDetailsRoute(
$extra: document,
title: document.title,
id: document.id,
thumbnailUrl: document.buildThumbnailUrl(context),
isLabelClickable: false,
).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/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/routes/navigation_keys.dart';
import 'package:paperless_mobile/routes/routes.dart';
import 'package:paperless_mobile/theme.dart';
class DocumentsBranch extends StatefulShellBranchData {
@@ -33,14 +32,18 @@ class DocumentDetailsRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey =
outerShellNavigatorKey;
final int id;
final bool isLabelClickable;
final DocumentModel $extra;
final String? queryString;
final String? thumbnailUrl;
final String? title;
const DocumentDetailsRoute({
required this.$extra,
required this.id,
this.isLabelClickable = true,
this.queryString,
this.thumbnailUrl,
this.title,
});
@override
@@ -51,14 +54,15 @@ class DocumentDetailsRoute extends GoRouteData {
context.read(),
context.read(),
context.read(),
initialDocument: $extra,
)
..loadFullContent()
..loadMetaData(),
id: id,
)..initialize(),
lazy: false,
child: DocumentDetailsPage(
id: id,
isLabelClickable: isLabelClickable,
titleAndContentQueryString: queryString,
thumbnailUrl: thumbnailUrl,
title: title,
),
);
}
@@ -96,20 +100,19 @@ class EditDocumentRoute extends GoRouteData {
class DocumentPreviewRoute extends GoRouteData {
static final GlobalKey<NavigatorState> $parentNavigatorKey =
outerShellNavigatorKey;
final DocumentModel $extra;
final int id;
final String? title;
const DocumentPreviewRoute({
required this.$extra,
required this.id,
this.title,
});
@override
Widget build(BuildContext context, GoRouterState state) {
return DocumentView(
documentBytes: context.read<PaperlessDocumentsApi>().download($extra),
title: title ?? $extra.title,
documentBytes: context.read<PaperlessDocumentsApi>().downloadDocument(id),
title: title,
);
}
}

View File

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

View File

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

View File

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