mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 07:15:47 -06:00
feat: Add improved date input, fix bugs, restructurings
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// }
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]!,
|
||||||
|
|||||||
@@ -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>',
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user