diff --git a/lib/core/widgets/form_builder_fields/extended_date_range_dialog.dart b/lib/core/widgets/form_builder_fields/extended_date_range_dialog.dart new file mode 100644 index 0000000..49abee3 --- /dev/null +++ b/lib/core/widgets/form_builder_fields/extended_date_range_dialog.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_relative_date_range_field.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; + +class ExtendedDateRangeDialog extends StatefulWidget { + final DateRangeQuery initialValue; + final String Function(DateRangeQuery query) stringTransformer; + const ExtendedDateRangeDialog({ + super.key, + required this.initialValue, + required this.stringTransformer, + }); + + @override + State createState() => + _ExtendedDateRangeDialogState(); +} + +class _ExtendedDateRangeDialogState extends State { + static const String _fkAbsoluteBefore = 'absoluteBefore'; + static const String _fkAbsoluteAfter = 'absoluteAfter'; + static const String _fkRelative = 'relative'; + + final _formKey = GlobalKey(); + late DateRangeType _selectedDateRangeType; + @override + void initState() { + super.initState(); + _selectedDateRangeType = (widget.initialValue is RelativeDateRangeQuery) + ? DateRangeType.relative + : DateRangeType.absolute; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Select date range"), + content: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Hint: You can either specify absolute values by selecting concrete dates, or you can specify a time range relative to today.", + style: Theme.of(context).textTheme.caption, + ), + _buildDateRangeQueryTypeSelection(), + const SizedBox(height: 16), + Builder( + builder: (context) { + switch (_selectedDateRangeType) { + case DateRangeType.absolute: + return _buildAbsoluteDateRangeForm(); + case DateRangeType.relative: + return FormBuilderRelativeDateRangePicker( + initialValue: + widget.initialValue is RelativeDateRangeQuery + ? widget.initialValue as RelativeDateRangeQuery + : const RelativeDateRangeQuery( + 1, + DateRangeUnit.month, + ), + name: _fkRelative, + ); + } + }, + ), + ], + ), + ), + actions: [ + TextButton( + child: Text(S.of(context).genericActionCancelLabel), + onPressed: () => Navigator.pop(context), + ), + TextButton( + child: Text(S.of(context).genericActionSaveLabel), + onPressed: () { + _formKey.currentState?.save(); + if (_formKey.currentState?.validate() ?? false) { + final values = _formKey.currentState!.value; + final query = _buildQuery(values); + Navigator.pop(context, query); + } + }, + ), + ], + ); + } + + Widget _buildDateRangeQueryTypeSelection() { + return Row( + children: [ + ChoiceChip( + label: Text('Absolute'), + selected: _selectedDateRangeType == DateRangeType.absolute, + onSelected: (value) => + setState(() => _selectedDateRangeType = DateRangeType.absolute), + ).paddedOnly(right: 8.0), + ChoiceChip( + label: Text('Relative'), + selected: _selectedDateRangeType == DateRangeType.relative, + onSelected: (value) => + setState(() => _selectedDateRangeType = DateRangeType.relative), + ), + ], + ); + } + + Widget _buildAbsoluteDateRangeForm() { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + FormBuilderDateTimePicker( + name: _fkAbsoluteAfter, + initialDate: widget.initialValue is AbsoluteDateRangeQuery + ? (widget.initialValue as AbsoluteDateRangeQuery).after + : null, + decoration: InputDecoration( + labelText: S.of(context).extendedDateRangePickerAfterLabel, + ), + inputType: InputType.date, + ), + FormBuilderDateTimePicker( + name: _fkAbsoluteBefore, + initialDate: widget.initialValue is AbsoluteDateRangeQuery + ? (widget.initialValue as AbsoluteDateRangeQuery).before + : null, + inputType: InputType.date, + decoration: InputDecoration( + labelText: S.of(context).extendedDateRangePickerBeforeLabel, + ), + ), + ], + ); + } + + DateRangeQuery? _buildQuery(Map values) { + if (_selectedDateRangeType == DateRangeType.absolute) { + return AbsoluteDateRangeQuery( + after: values[_fkAbsoluteAfter], + before: values[_fkAbsoluteBefore], + ); + } else { + return values[_fkRelative] as RelativeDateRangeQuery; + } + } +} + +enum DateRangeType { + absolute, + relative; +} diff --git a/lib/core/widgets/form_builder_fields/form_builder_extended_date_range_picker.dart b/lib/core/widgets/form_builder_fields/form_builder_extended_date_range_picker.dart new file mode 100644 index 0000000..f3a3201 --- /dev/null +++ b/lib/core/widgets/form_builder_fields/form_builder_extended_date_range_picker.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_dialog.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class FormBuilderExtendedDateRangePicker extends StatefulWidget { + final String name; + final String labelText; + final DateRangeQuery initialValue; + + const FormBuilderExtendedDateRangePicker({ + super.key, + required this.name, + required this.labelText, + required this.initialValue, + }); + + @override + State createState() => + _FormBuilderExtendedDateRangePickerState(); +} + +class _FormBuilderExtendedDateRangePickerState + extends State { + late final TextEditingController _textEditingController; + + @override + void initState() { + super.initState(); + _textEditingController = TextEditingController( + text: _dateRangeQueryToString(widget.initialValue)); + } + + @override + Widget build(BuildContext context) { + return FormBuilderField( + name: widget.name, + initialValue: widget.initialValue, + onChanged: (query) { + _textEditingController.text = + _dateRangeQueryToString(query ?? const UnsetDateRangeQuery()); + }, + builder: (field) { + return Column( + children: [ + TextFormField( + controller: _textEditingController, + readOnly: true, + onTap: () => _showExtendedDateRangePicker(field), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.date_range), + labelText: widget.labelText, + ), + ), + _buildExtendedQueryOptions(field), + ], + ); + }, + ); + } + + Widget _buildExtendedQueryOptions(FormFieldState field) { + return SizedBox( + height: 64, + child: ListView.separated( + itemCount: _options.length, + separatorBuilder: (context, index) => const SizedBox(width: 8.0), + itemBuilder: (context, index) { + final option = _options[index]; + return FilterChip( + label: Text(option.title), + onSelected: (isSelected) => isSelected + ? field.didChange(option.value) + : field.didChange(const UnsetDateRangeQuery()), + selected: field.value == option.value, + ); + }, + scrollDirection: Axis.horizontal, + ), + ); + } + + List<_ExtendedDateRangeQueryOption> get _options => [ + _ExtendedDateRangeQueryOption( + S.of(context).extendedDateRangePickerLastWeeksLabel(1), + const RelativeDateRangeQuery(1, DateRangeUnit.week), + ), + _ExtendedDateRangeQueryOption( + S.of(context).extendedDateRangePickerLastMonthsLabel(1), + const RelativeDateRangeQuery(1, DateRangeUnit.month), + ), + _ExtendedDateRangeQueryOption( + S.of(context).extendedDateRangePickerLastMonthsLabel(3), + const RelativeDateRangeQuery(3, DateRangeUnit.month), + ), + _ExtendedDateRangeQueryOption( + S.of(context).extendedDateRangePickerLastYearsLabel(1), + const RelativeDateRangeQuery(1, DateRangeUnit.year), + ), + ]; + + String _dateRangeQueryToString(DateRangeQuery query) { + if (query is UnsetDateRangeQuery) { + return ''; + } else if (query is AbsoluteDateRangeQuery) { + if (query.before != null && query.after != null) { + return '${DateFormat.yMd(query.after)} – ${DateFormat.yMd(query.before)}'; + } + if (query.before != null) { + return '${S.of(context).extendedDateRangePickerBeforeLabel} ${DateFormat.yMd(query.before)}'; + } + if (query.after != null) { + return '${S.of(context).extendedDateRangePickerAfterLabel} ${DateFormat.yMd(query.after)}'; + } + } else if (query is RelativeDateRangeQuery) { + switch (query.unit) { + case DateRangeUnit.day: + return S + .of(context) + .extendedDateRangePickerLastDaysLabel(query.offset); + case DateRangeUnit.week: + return S + .of(context) + .extendedDateRangePickerLastWeeksLabel(query.offset); + case DateRangeUnit.month: + return S + .of(context) + .extendedDateRangePickerLastMonthsLabel(query.offset); + case DateRangeUnit.year: + return S + .of(context) + .extendedDateRangePickerLastYearsLabel(query.offset); + default: + } + } + return ''; + } + + void _showExtendedDateRangePicker( + FormFieldState field, + ) async { + final query = await showDialog( + context: context, + builder: (context) => ExtendedDateRangeDialog( + initialValue: field.value!, + stringTransformer: _dateRangeQueryToString, + ), + ); + if (query != null) { + field.didChange(query); + } + } +} + +class _ExtendedDateRangeQueryOption { + final String title; + final RelativeDateRangeQuery value; + + _ExtendedDateRangeQueryOption(this.title, this.value); +} diff --git a/lib/core/widgets/form_builder_fields/form_builder_relative_date_range_field.dart b/lib/core/widgets/form_builder_fields/form_builder_relative_date_range_field.dart new file mode 100644 index 0000000..24cd009 --- /dev/null +++ b/lib/core/widgets/form_builder_fields/form_builder_relative_date_range_field.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class FormBuilderRelativeDateRangePicker extends StatefulWidget { + final String name; + final RelativeDateRangeQuery initialValue; + const FormBuilderRelativeDateRangePicker({ + super.key, + required this.name, + required this.initialValue, + }); + + @override + State createState() => + _FormBuilderRelativeDateRangePickerState(); +} + +class _FormBuilderRelativeDateRangePickerState + extends State { + late int _offset; + @override + void initState() { + super.initState(); + _offset = widget.initialValue.offset; + } + + @override + Widget build(BuildContext context) { + return FormBuilderField( + name: widget.name, + initialValue: widget.initialValue, + builder: (field) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Last"), + SizedBox( + width: 70, + child: TextFormField( + decoration: InputDecoration( + labelText: "Offset", + ), + initialValue: widget.initialValue.offset.toString(), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[1-9][0-9]*")) + ], + validator: FormBuilderValidators.numeric(), + keyboardType: TextInputType.number, + onChanged: (value) { + final parsed = int.parse(value); + setState(() { + _offset = parsed; + }); + field.didChange(field.value!.copyWith(offset: parsed)); + }, + ), + ), + SizedBox( + width: 120, + child: DropdownButtonFormField( + value: field.value?.unit, + items: DateRangeUnit.values + .map( + (unit) => DropdownMenuItem( + child: Text( + _dateRangeUnitToLocalizedString( + unit, + _offset, + ), + ), + value: unit, + ), + ) + .toList(), + onChanged: (value) => + field.didChange(field.value!.copyWith(unit: value)), + decoration: InputDecoration( + labelText: "Amount", + ), + ), + ) + ], + ), + ); + } + + String _dateRangeUnitToLocalizedString(DateRangeUnit unit, int? count) { + switch (unit) { + case DateRangeUnit.day: + return S.of(context).extendedDateRangePickerDayText(count ?? 1); + case DateRangeUnit.week: + return S.of(context).extendedDateRangePickerWeekText(count ?? 1); + case DateRangeUnit.month: + return S.of(context).extendedDateRangePickerMonthText(count ?? 1); + case DateRangeUnit.year: + return S.of(context).extendedDateRangePickerYearText(count ?? 1); + } + } +} diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index 7050060..a53b83a 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -162,7 +162,7 @@ class _DocumentUploadPreparationPageState S.of(context).documentCreatedPropertyLabel + " *", ), ), - LabelFormField( + LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => @@ -172,15 +172,13 @@ class _DocumentUploadPreparationPageState ), child: AddDocumentTypePage(initialName: initialName), ), - label: S.of(context).documentDocumentTypePropertyLabel + " *", + textFieldLabel: + S.of(context).documentDocumentTypePropertyLabel + " *", name: DocumentModel.documentTypeKey, - state: state.documentTypes, - queryParameterIdBuilder: DocumentTypeQuery.fromId, - queryParameterNotAssignedBuilder: - DocumentTypeQuery.notAssigned, + labelOptions: state.documentTypes, prefixIcon: const Icon(Icons.description_outlined), ), - LabelFormField( + LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialName) => @@ -191,13 +189,10 @@ class _DocumentUploadPreparationPageState ), child: AddCorrespondentPage(initialName: initialName), ), - label: + textFieldLabel: S.of(context).documentCorrespondentPropertyLabel + " *", name: DocumentModel.correspondentKey, - state: state.correspondents, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: - CorrespondentQuery.notAssigned, + labelOptions: state.correspondents, prefixIcon: const Icon(Icons.person_outline), ), TagFormField( diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 6389ed6..954b2ea 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -96,26 +96,24 @@ class _DocumentEditPageState extends State { Widget _buildStoragePathFormField( int? initialId, Map options) { - return LabelFormField( + return LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value( value: RepositoryProvider.of>(context), child: AddStoragePathPage(initalValue: initialValue), ), - label: S.of(context).documentStoragePathPropertyLabel, - state: options, - initialValue: StoragePathQuery.fromId(initialId), + textFieldLabel: S.of(context).documentStoragePathPropertyLabel, + labelOptions: options, + initialValue: IdQueryParameter.fromId(initialId), name: fkStoragePath, - queryParameterIdBuilder: StoragePathQuery.fromId, - queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, prefixIcon: const Icon(Icons.folder_outlined), ); } Widget _buildCorrespondentFormField( int? initialId, Map options) { - return LabelFormField( + return LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value( @@ -124,19 +122,17 @@ class _DocumentEditPageState extends State { ), child: AddCorrespondentPage(initialName: initialValue), ), - label: S.of(context).documentCorrespondentPropertyLabel, - state: options, - initialValue: CorrespondentQuery.fromId(initialId), + textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, + labelOptions: options, + initialValue: IdQueryParameter.fromId(initialId), name: fkCorrespondent, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, prefixIcon: const Icon(Icons.person_outlined), ); } Widget _buildDocumentTypeFormField( int? initialId, Map options) { - return LabelFormField( + return LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (currentInput) => RepositoryProvider.value( @@ -147,12 +143,10 @@ class _DocumentEditPageState extends State { initialName: currentInput, ), ), - label: S.of(context).documentDocumentTypePropertyLabel, - initialValue: DocumentTypeQuery.fromId(initialId), - state: options, + textFieldLabel: S.of(context).documentDocumentTypePropertyLabel, + initialValue: IdQueryParameter.fromId(initialId), + labelOptions: options, name: fkDocumentType, - queryParameterIdBuilder: DocumentTypeQuery.fromId, - queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, prefixIcon: const Icon(Icons.description_outlined), ); } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 4450591..531c6ae 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -121,6 +121,7 @@ class _DocumentsPageState extends State { expand: false, snap: true, initialChildSize: .9, + maxChildSize: .9, builder: (context, controller) => LabelsBlocProvider( child: DocumentFilterPanel( initialFilter: _documentsCubit.state.filter, diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 65d06b0..28d8a89 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_extended_date_range_picker.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; @@ -95,8 +96,13 @@ class _DocumentFilterPanelState extends State { style: Theme.of(context).textTheme.caption, ), ).padded(), - _buildCreatedDateRangePickerFormField(), - _buildAddedDateRangePickerFormField(), + FormBuilderExtendedDateRangePicker( + name: DocumentModel.createdKey, + initialValue: widget.initialFilter.created, + labelText: S.of(context).documentCreatedPropertyLabel, + ).padded(), + // _buildCreatedDateRangePickerFormField(), + // _buildAddedDateRangePickerFormField(), _buildCorrespondentFormField().padded(), _buildDocumentTypeFormField().padded(), _buildStoragePathFormField().padded(), @@ -129,14 +135,12 @@ class _DocumentFilterPanelState extends State { Widget _buildDocumentTypeFormField() { return BlocBuilder, LabelState>( builder: (context, state) { - return LabelFormField( + return LabelFormField( formBuilderState: _formKey.currentState, name: fkDocumentType, - state: state.labels, - label: S.of(context).documentDocumentTypePropertyLabel, + labelOptions: state.labels, + textFieldLabel: S.of(context).documentDocumentTypePropertyLabel, initialValue: widget.initialFilter.documentType, - queryParameterIdBuilder: DocumentTypeQuery.fromId, - queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, prefixIcon: const Icon(Icons.description_outlined), ); }, @@ -146,14 +150,12 @@ class _DocumentFilterPanelState extends State { Widget _buildCorrespondentFormField() { return BlocBuilder, LabelState>( builder: (context, state) { - return LabelFormField( + return LabelFormField( formBuilderState: _formKey.currentState, name: fkCorrespondent, - state: state.labels, - label: S.of(context).documentCorrespondentPropertyLabel, + labelOptions: state.labels, + textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, initialValue: widget.initialFilter.correspondent, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, prefixIcon: const Icon(Icons.person_outline), ); }, @@ -163,14 +165,12 @@ class _DocumentFilterPanelState extends State { Widget _buildStoragePathFormField() { return BlocBuilder, LabelState>( builder: (context, state) { - return LabelFormField( + return LabelFormField( formBuilderState: _formKey.currentState, name: fkStoragePath, - state: state.labels, - label: S.of(context).documentStoragePathPropertyLabel, + labelOptions: state.labels, + textFieldLabel: S.of(context).documentStoragePathPropertyLabel, initialValue: widget.initialFilter.storagePath, - queryParameterIdBuilder: StoragePathQuery.fromId, - queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, prefixIcon: const Icon(Icons.folder_outlined), ); }, @@ -209,187 +209,99 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildDateRangePickerHelper(String formFieldKey) { - const spacer = SizedBox(width: 8.0); - return SizedBox( - height: 64, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - spacer, - ActionChip( - label: Text( - S.of(context).documentFilterDateRangeLastSevenDaysLabel, - ), - onPressed: () { - _formKey.currentState?.fields[formFieldKey]?.didChange( - DateTimeRange( - start: DateUtils.addDaysToDate(DateTime.now(), -7), - end: DateTime.now(), - ), - ); - }, - ), - spacer, - ActionChip( - label: Text( - S.of(context).documentFilterDateRangeLastMonthLabel, - ), - onPressed: () { - final now = DateTime.now(); - final firstDayOfLastMonth = - DateUtils.addMonthsToMonthDate(now, -1); - _formKey.currentState?.fields[formFieldKey]?.didChange( - DateTimeRange( - start: DateTime(firstDayOfLastMonth.year, - firstDayOfLastMonth.month, now.day), - end: DateTime.now(), - ), - ); - }, - ), - spacer, - ActionChip( - label: Text( - S.of(context).documentFilterDateRangeLastThreeMonthsLabel, - ), - onPressed: () { - final now = DateTime.now(); - final firstDayOfLastMonth = - DateUtils.addMonthsToMonthDate(now, -3); - _formKey.currentState?.fields[formFieldKey]?.didChange( - DateTimeRange( - start: DateTime( - firstDayOfLastMonth.year, - firstDayOfLastMonth.month, - now.day, - ), - end: DateTime.now(), - ), - ); - }, - ), - spacer, - ActionChip( - label: Text( - S.of(context).documentFilterDateRangeLastYearLabel, - ), - onPressed: () { - final now = DateTime.now(); - final firstDayOfLastMonth = - DateUtils.addMonthsToMonthDate(now, -12); - _formKey.currentState?.fields[formFieldKey]?.didChange( - DateTimeRange( - start: DateTime( - firstDayOfLastMonth.year, - firstDayOfLastMonth.month, - now.day, - ), - end: DateTime.now(), - ), - ); - }, - ), - spacer, - ], - ), - ); - } + // Widget _buildCreatedDateRangePickerFormField() { + // return Column( + // children: [ + // FormBuilderDateRangePicker( + // initialValue: _dateTimeRangeOfNullable( + // widget.initialFilter.createdDateAfter, + // widget.initialFilter.createdDateBefore, + // ), + // // Workaround for theme data not being correctly passed to daterangepicker, see + // // https://github.com/flutter/flutter/issues/87580 + // pickerBuilder: (context, Widget? child) => Theme( + // data: Theme.of(context).copyWith( + // dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor, + // appBarTheme: Theme.of(context).appBarTheme.copyWith( + // iconTheme: + // IconThemeData(color: Theme.of(context).primaryColor), + // ), + // colorScheme: Theme.of(context).colorScheme.copyWith( + // onPrimary: Theme.of(context).primaryColor, + // primary: Theme.of(context).colorScheme.primary, + // ), + // ), + // child: child!, + // ), + // format: DateFormat.yMMMd(Localizations.localeOf(context).toString()), + // fieldStartLabelText: + // S.of(context).documentFilterDateRangeFieldStartLabel, + // fieldEndLabelText: S.of(context).documentFilterDateRangeFieldEndLabel, + // firstDate: DateTime.fromMicrosecondsSinceEpoch(0), + // lastDate: DateTime.now(), + // name: fkCreatedAt, + // decoration: InputDecoration( + // prefixIcon: const Icon(Icons.calendar_month_outlined), + // labelText: S.of(context).documentCreatedPropertyLabel, + // suffixIcon: IconButton( + // icon: const Icon(Icons.clear), + // onPressed: () { + // _formKey.currentState?.fields[fkCreatedAt]?.didChange(null); + // }, + // ), + // ), + // ).paddedSymmetrically(horizontal: 8, vertical: 4.0), + // ], + // ); + // } - Widget _buildCreatedDateRangePickerFormField() { - return Column( - children: [ - FormBuilderDateRangePicker( - initialValue: _dateTimeRangeOfNullable( - widget.initialFilter.createdDateAfter, - widget.initialFilter.createdDateBefore, - ), - // Workaround for theme data not being correctly passed to daterangepicker, see - // https://github.com/flutter/flutter/issues/87580 - pickerBuilder: (context, Widget? child) => Theme( - data: Theme.of(context).copyWith( - dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBarTheme: Theme.of(context).appBarTheme.copyWith( - iconTheme: - IconThemeData(color: Theme.of(context).primaryColor), - ), - colorScheme: Theme.of(context).colorScheme.copyWith( - onPrimary: Theme.of(context).primaryColor, - primary: Theme.of(context).colorScheme.primary, - ), - ), - child: child!, - ), - format: DateFormat.yMMMd(Localizations.localeOf(context).toString()), - fieldStartLabelText: - S.of(context).documentFilterDateRangeFieldStartLabel, - fieldEndLabelText: S.of(context).documentFilterDateRangeFieldEndLabel, - firstDate: DateTime.fromMicrosecondsSinceEpoch(0), - lastDate: DateTime.now(), - name: fkCreatedAt, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.calendar_month_outlined), - labelText: S.of(context).documentCreatedPropertyLabel, - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _formKey.currentState?.fields[fkCreatedAt]?.didChange(null); - }, - ), - ), - ).paddedSymmetrically(horizontal: 8, vertical: 4.0), - _buildDateRangePickerHelper(fkCreatedAt), - ], - ); - } - - Widget _buildAddedDateRangePickerFormField() { - return Column( - children: [ - FormBuilderDateRangePicker( - initialValue: _dateTimeRangeOfNullable( - widget.initialFilter.addedDateAfter, - widget.initialFilter.addedDateBefore, - ), - // Workaround for theme data not being correctly passed to daterangepicker, see - // https://github.com/flutter/flutter/issues/87580 - pickerBuilder: (context, Widget? child) => Theme( - data: Theme.of(context).copyWith( - dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBarTheme: Theme.of(context).appBarTheme.copyWith( - iconTheme: - IconThemeData(color: Theme.of(context).primaryColor), - ), - colorScheme: Theme.of(context).colorScheme.copyWith( - onPrimary: Theme.of(context).primaryColor, - primary: Theme.of(context).colorScheme.primary, - ), - ), - child: child!, - ), - format: DateFormat.yMMMd(), - fieldStartLabelText: - S.of(context).documentFilterDateRangeFieldStartLabel, - fieldEndLabelText: S.of(context).documentFilterDateRangeFieldEndLabel, - firstDate: DateTime.fromMicrosecondsSinceEpoch(0), - lastDate: DateTime.now(), - name: fkAddedAt, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.calendar_month_outlined), - labelText: S.of(context).documentAddedPropertyLabel, - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _formKey.currentState?.fields[fkAddedAt]?.didChange(null); - }, - ), - ), - ).paddedSymmetrically(horizontal: 8), - const SizedBox(height: 4.0), - _buildDateRangePickerHelper(fkAddedAt), - ], - ); - } + // Widget _buildAddedDateRangePickerFormField() { + // return Column( + // children: [ + // FormBuilderDateRangePicker( + // initialValue: _dateTimeRangeOfNullable( + // widget.initialFilter.addedDateAfter, + // widget.initialFilter.addedDateBefore, + // ), + // // Workaround for theme data not being correctly passed to daterangepicker, see + // // https://github.com/flutter/flutter/issues/87580 + // pickerBuilder: (context, Widget? child) => Theme( + // data: Theme.of(context).copyWith( + // dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor, + // appBarTheme: Theme.of(context).appBarTheme.copyWith( + // iconTheme: + // IconThemeData(color: Theme.of(context).primaryColor), + // ), + // colorScheme: Theme.of(context).colorScheme.copyWith( + // onPrimary: Theme.of(context).primaryColor, + // primary: Theme.of(context).colorScheme.primary, + // ), + // ), + // child: child!, + // ), + // format: DateFormat.yMMMd(), + // fieldStartLabelText: + // S.of(context).documentFilterDateRangeFieldStartLabel, + // fieldEndLabelText: S.of(context).documentFilterDateRangeFieldEndLabel, + // firstDate: DateTime.fromMicrosecondsSinceEpoch(0), + // lastDate: DateTime.now(), + // name: fkAddedAt, + // decoration: InputDecoration( + // prefixIcon: const Icon(Icons.calendar_month_outlined), + // labelText: S.of(context).documentAddedPropertyLabel, + // suffixIcon: IconButton( + // icon: const Icon(Icons.clear), + // onPressed: () { + // _formKey.currentState?.fields[fkAddedAt]?.didChange(null); + // }, + // ), + // ), + // ).paddedSymmetrically(horizontal: 8), + // const SizedBox(height: 4.0), + // _buildDateRangePickerHelper(fkAddedAt), + // ], + // ); + // } void _onApplyFilter() async { _formKey.currentState?.save(); @@ -408,19 +320,17 @@ class _DocumentFilterPanelState extends State { DocumentFilter _assembleFilter() { final v = _formKey.currentState!.value; return DocumentFilter( - createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end, - createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start, - correspondent: v[fkCorrespondent] as CorrespondentQuery? ?? + created: (v[fkCreatedAt] as DateRangeQuery), + correspondent: v[fkCorrespondent] as IdQueryParameter? ?? DocumentFilter.initial.correspondent, - documentType: v[fkDocumentType] as DocumentTypeQuery? ?? + documentType: v[fkDocumentType] as IdQueryParameter? ?? DocumentFilter.initial.documentType, - storagePath: v[fkStoragePath] as StoragePathQuery? ?? + storagePath: v[fkStoragePath] as IdQueryParameter? ?? DocumentFilter.initial.storagePath, tags: v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags, queryText: v[fkQuery] as String?, - addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end, - addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start, + added: (v[fkAddedAt] as DateRangeQuery), queryType: v[QueryTypeFormField.fkQueryType] as QueryType, asnQuery: widget.initialFilter.asnQuery, page: 1, diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 296a0ec..0ea49df 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -52,12 +52,12 @@ class CorrespondentWidget extends StatelessWidget { if (cubit.state.filter.correspondent.id == correspondentId) { cubit.updateCurrentFilter( (filter) => - filter.copyWith(correspondent: const CorrespondentQuery.unset()), + filter.copyWith(correspondent: const IdQueryParameter.unset()), ); } else { cubit.updateCurrentFilter( (filter) => filter.copyWith( - correspondent: CorrespondentQuery.fromId(correspondentId)), + correspondent: IdQueryParameter.fromId(correspondentId)), ); } afterSelected?.call(); diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index b0108ab..72cb54b 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -51,12 +51,12 @@ class DocumentTypeWidget extends StatelessWidget { if (cubit.state.filter.documentType.id == documentTypeId) { cubit.updateCurrentFilter( (filter) => - filter.copyWith(documentType: const DocumentTypeQuery.unset()), + filter.copyWith(documentType: const IdQueryParameter.unset()), ); } else { cubit.updateCurrentFilter( (filter) => filter.copyWith( - documentType: DocumentTypeQuery.fromId(documentTypeId)), + documentType: IdQueryParameter.fromId(documentTypeId)), ); } afterSelected?.call(); diff --git a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart index ce3fd07..c290d79 100644 --- a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart +++ b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart @@ -54,12 +54,12 @@ class StoragePathWidget extends StatelessWidget { if (cubit.state.filter.correspondent.id == pathId) { cubit.updateCurrentFilter( (filter) => - filter.copyWith(storagePath: const StoragePathQuery.unset()), + filter.copyWith(storagePath: const IdQueryParameter.unset()), ); } else { cubit.updateCurrentFilter( (filter) => - filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)), + filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)), ); } afterSelected?.call(); diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 0c4e4fb..18c29ed 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -126,7 +126,7 @@ class _LabelsPageState extends State ), child: LabelTabView( filterBuilder: (label) => DocumentFilter( - correspondent: CorrespondentQuery.fromId(label.id), + correspondent: IdQueryParameter.fromId(label.id), pageSize: label.documentCount ?? 0, ), onEdit: _openEditCorrespondentPage, @@ -146,7 +146,7 @@ class _LabelsPageState extends State ), child: LabelTabView( filterBuilder: (label) => DocumentFilter( - documentType: DocumentTypeQuery.fromId(label.id), + documentType: IdQueryParameter.fromId(label.id), pageSize: label.documentCount ?? 0, ), onEdit: _openEditDocumentTypePage, @@ -194,7 +194,7 @@ class _LabelsPageState extends State child: LabelTabView( onEdit: _openEditStoragePathPage, filterBuilder: (label) => DocumentFilter( - storagePath: StoragePathQuery.fromId(label.id), + storagePath: IdQueryParameter.fromId(label.id), pageSize: label.documentCount ?? 0, ), contentBuilder: (path) => Text(path.path ?? ""), diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index 0e62bd5..09857cd 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -9,31 +9,26 @@ import 'package:paperless_mobile/generated/l10n.dart'; /// Form field allowing to select labels (i.e. correspondent, documentType) /// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...). /// -class LabelFormField - extends StatefulWidget { +class LabelFormField extends StatefulWidget { final Widget prefixIcon; - final Map state; + final Map labelOptions; final FormBuilderState? formBuilderState; final IdQueryParameter? initialValue; final String name; - final String label; + final String textFieldLabel; final FormFieldValidator? validator; - final Widget Function(String)? labelCreationWidgetBuilder; - final R Function() queryParameterNotAssignedBuilder; - final R Function(int? id) queryParameterIdBuilder; + final Widget Function(String initialName)? labelCreationWidgetBuilder; final bool notAssignedSelectable; - final void Function(R?)? onChanged; + final void Function(IdQueryParameter?)? onChanged; const LabelFormField({ Key? key, required this.name, - required this.state, + required this.labelOptions, this.validator, this.initialValue, - required this.label, + required this.textFieldLabel, this.labelCreationWidgetBuilder, - required this.queryParameterNotAssignedBuilder, - required this.queryParameterIdBuilder, required this.formBuilderState, required this.prefixIcon, this.notAssignedSelectable = true, @@ -41,11 +36,10 @@ class LabelFormField }) : super(key: key); @override - State> createState() => _LabelFormFieldState(); + State> createState() => _LabelFormFieldState(); } -class _LabelFormFieldState - extends State> { +class _LabelFormFieldState extends State> { bool _showCreationSuffixIcon = false; late bool _showClearSuffixIcon; @@ -54,12 +48,13 @@ class _LabelFormFieldState @override void initState() { super.initState(); - _showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id); + _showClearSuffixIcon = + widget.labelOptions.containsKey(widget.initialValue?.id); _textEditingController = TextEditingController( - text: widget.state[widget.initialValue?.id]?.name ?? '', + text: widget.labelOptions[widget.initialValue?.id]?.name ?? '', )..addListener(() { setState(() { - _showCreationSuffixIcon = widget.state.values + _showCreationSuffixIcon = widget.labelOptions.values .where( (item) => item.name.toLowerCase().startsWith( _textEditingController.text.toLowerCase(), @@ -74,7 +69,7 @@ class _LabelFormFieldState @override Widget build(BuildContext context) { - final isEnabled = widget.state.values.fold( + final isEnabled = widget.labelOptions.values.fold( false, (previousValue, element) => previousValue || (element.documentCount ?? 0) > 0) || @@ -90,7 +85,7 @@ class _LabelFormFieldState TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0), ), ), - initialValue: widget.initialValue ?? widget.queryParameterIdBuilder(null), + initialValue: widget.initialValue ?? const IdQueryParameter.unset(), name: widget.name, suggestionsBoxDecoration: SuggestionsBoxDecoration( shape: RoundedRectangleBorder( @@ -103,7 +98,7 @@ class _LabelFormFieldState ), itemBuilder: (context, suggestion) => ListTile( title: Text( - widget.state[suggestion.id]?.name ?? + widget.labelOptions[suggestion.id]?.name ?? S.of(context).labelNotAssignedText, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -112,10 +107,10 @@ class _LabelFormFieldState style: ListTileStyle.list, ), suggestionsCallback: (pattern) { - final List suggestions = widget.state.entries + final List suggestions = widget.labelOptions.entries .where( (entry) => - widget.state[entry.key]!.name + widget.labelOptions[entry.key]!.name .toLowerCase() .contains(pattern.toLowerCase()) || pattern.isEmpty, @@ -125,34 +120,33 @@ class _LabelFormFieldState widget.labelCreationWidgetBuilder != null || (entry.value.documentCount ?? 0) > 0, ) - .map((entry) => widget.queryParameterIdBuilder(entry.key)) + .map((entry) => IdQueryParameter.fromId(entry.key)) .toList(); if (widget.notAssignedSelectable) { - suggestions.insert(0, widget.queryParameterNotAssignedBuilder()); + suggestions.insert(0, const IdQueryParameter.notAssigned()); } return suggestions; }, onChanged: (value) { setState(() => _showClearSuffixIcon = value?.isSet ?? false); - widget.onChanged?.call(value as R); + widget.onChanged?.call(value); }, controller: _textEditingController, decoration: InputDecoration( prefixIcon: widget.prefixIcon, - label: Text(widget.label), + label: Text(widget.textFieldLabel), hintText: _getLocalizedHint(context), suffixIcon: _buildSuffixIcon(context), ), selectionToTextTransformer: (suggestion) { - if (suggestion == widget.queryParameterNotAssignedBuilder()) { + if (suggestion == const IdQueryParameter.notAssigned()) { return S.of(context).labelNotAssignedText; } - return widget.state[suggestion.id]?.name ?? ""; + return widget.labelOptions[suggestion.id]?.name ?? ""; }, direction: AxisDirection.up, - onSuggestionSelected: (suggestion) => widget - .formBuilderState?.fields[widget.name] - ?.didChange(suggestion as R), + onSuggestionSelected: (suggestion) => + widget.formBuilderState?.fields[widget.name]?.didChange(suggestion), ); } @@ -161,7 +155,7 @@ class _LabelFormFieldState return IconButton( onPressed: () async { FocusScope.of(context).unfocus(); - final createdLabel = await showDialog( + final createdLabel = await showDialog( context: context, builder: (context) => widget.labelCreationWidgetBuilder!( _textEditingController.text, @@ -170,7 +164,7 @@ class _LabelFormFieldState if (createdLabel != null) { // If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done). widget.formBuilderState?.fields[widget.name] - ?.didChange(widget.queryParameterIdBuilder(createdLabel.id)); + ?.didChange(IdQueryParameter.fromId(createdLabel.id)); _textEditingController.text = createdLabel.name; } else { _reset(); @@ -192,7 +186,7 @@ class _LabelFormFieldState void _reset() { widget.formBuilderState?.fields[widget.name]?.didChange( - widget.queryParameterIdBuilder(null), // equivalnt to IdQueryParam.unset() + const IdQueryParameter.unset(), ); _textEditingController.clear(); } diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index dbfe0cd..a49856a 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -78,18 +78,6 @@ "@documentFilterAdvancedLabel": {}, "documentFilterApplyFilterLabel": "Použít", "@documentFilterApplyFilterLabel": {}, - "documentFilterDateRangeFieldEndLabel": "Do", - "@documentFilterDateRangeFieldEndLabel": {}, - "documentFilterDateRangeFieldStartLabel": "Od", - "@documentFilterDateRangeFieldStartLabel": {}, - "documentFilterDateRangeLastMonthLabel": "Minulý měsíc", - "@documentFilterDateRangeLastMonthLabel": {}, - "documentFilterDateRangeLastSevenDaysLabel": "Posledních 7 dní", - "@documentFilterDateRangeLastSevenDaysLabel": {}, - "documentFilterDateRangeLastThreeMonthsLabel": "Poslední 3 měsíce", - "@documentFilterDateRangeLastThreeMonthsLabel": {}, - "documentFilterDateRangeLastYearLabel": "Minulý rok", - "@documentFilterDateRangeLastYearLabel": {}, "documentFilterQueryOptionsAsnLabel": "ASČ", "@documentFilterQueryOptionsAsnLabel": {}, "documentFilterQueryOptionsExtendedLabel": "Prodloužené", @@ -272,6 +260,64 @@ "@errorMessageUnsupportedFileFormat": {}, "errorReportLabel": "NAHLÁSIT", "@errorReportLabel": {}, + "extendedDateRangePickerAfterLabel": "", + "@extendedDateRangePickerAfterLabel": {}, + "extendedDateRangePickerBeforeLabel": "", + "@extendedDateRangePickerBeforeLabel": {}, + "extendedDateRangePickerDayText": "{count, plural, other{}}", + "@extendedDateRangePickerDayText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerFromLabel": "Od", + "@extendedDateRangePickerFromLabel": {}, + "extendedDateRangePickerLastDaysLabel": "{count, plural, zero{} one{} few{} many{} other{Posledních 7 dní}}", + "@extendedDateRangePickerLastDaysLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastMonthsLabel": "{count, plural, zero{} one{} few{} many{} other{Minulý měsíc}}", + "@extendedDateRangePickerLastMonthsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastText": "", + "@extendedDateRangePickerLastText": {}, + "extendedDateRangePickerLastWeeksLabel": "{count, plural, other{}}", + "@extendedDateRangePickerLastWeeksLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastYearsLabel": "{count, plural, zero{} one{} few{} many{} other{Minulý rok}}", + "@extendedDateRangePickerLastYearsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerMonthText": "{count, plural, other{}}", + "@extendedDateRangePickerMonthText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerToLabel": "Do", + "@extendedDateRangePickerToLabel": {}, + "extendedDateRangePickerWeekText": "{count, plural, other{}}", + "@extendedDateRangePickerWeekText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerYearText": "{count, plural, other{}}", + "@extendedDateRangePickerYearText": { + "placeholders": { + "count": {} + } + }, "genericActionCancelLabel": "Zrušit", "@genericActionCancelLabel": {}, "genericActionCreateLabel": "Vytvořit", @@ -408,7 +454,7 @@ "@savedViewShowOnDashboardLabel": {}, "savedViewsLabel": "Uložené náhledy", "@savedViewsLabel": {}, - "scannerPageImagePreviewTitle": "", + "scannerPageImagePreviewTitle": "Sken", "@scannerPageImagePreviewTitle": {}, "serverInformationPaperlessVersionText": "Verze Paperless serveru", "@serverInformationPaperlessVersionText": {}, diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 5bc6ef7..ed11e6b 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -78,18 +78,6 @@ "@documentFilterAdvancedLabel": {}, "documentFilterApplyFilterLabel": "Anwenden", "@documentFilterApplyFilterLabel": {}, - "documentFilterDateRangeFieldEndLabel": "Bis", - "@documentFilterDateRangeFieldEndLabel": {}, - "documentFilterDateRangeFieldStartLabel": "Von", - "@documentFilterDateRangeFieldStartLabel": {}, - "documentFilterDateRangeLastMonthLabel": "Letzter Monat", - "@documentFilterDateRangeLastMonthLabel": {}, - "documentFilterDateRangeLastSevenDaysLabel": "Letzte 7 Tage", - "@documentFilterDateRangeLastSevenDaysLabel": {}, - "documentFilterDateRangeLastThreeMonthsLabel": "Letzte 3 Monate", - "@documentFilterDateRangeLastThreeMonthsLabel": {}, - "documentFilterDateRangeLastYearLabel": "Letztes Jahr", - "@documentFilterDateRangeLastYearLabel": {}, "documentFilterQueryOptionsAsnLabel": "ASN", "@documentFilterQueryOptionsAsnLabel": {}, "documentFilterQueryOptionsExtendedLabel": "Erweitert", @@ -272,6 +260,64 @@ "@errorMessageUnsupportedFileFormat": {}, "errorReportLabel": "MELDEN", "@errorReportLabel": {}, + "extendedDateRangePickerAfterLabel": "Nach", + "@extendedDateRangePickerAfterLabel": {}, + "extendedDateRangePickerBeforeLabel": "Vor", + "@extendedDateRangePickerBeforeLabel": {}, + "extendedDateRangePickerDayText": "{count, plural, zero{} one{Tag} other{Tage}}", + "@extendedDateRangePickerDayText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerFromLabel": "Von", + "@extendedDateRangePickerFromLabel": {}, + "extendedDateRangePickerLastDaysLabel": "{count, plural, zero{} one{Gestern} other{Letzte {count} Tage}}", + "@extendedDateRangePickerLastDaysLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastMonthsLabel": "{count, plural, zero{} one{Letzter Monat} other{Letzte {count} Monate}}", + "@extendedDateRangePickerLastMonthsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastText": "Letzte", + "@extendedDateRangePickerLastText": {}, + "extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Letzte Woche} other{Letzte {count} Wochen}}", + "@extendedDateRangePickerLastWeeksLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastYearsLabel": "{count, plural, zero{} one{Letztes Jahr} other{Letzte {count} Jahre}}", + "@extendedDateRangePickerLastYearsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerMonthText": "{count, plural, zero{} one{Monat} other{Monate}}", + "@extendedDateRangePickerMonthText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerToLabel": "Bis", + "@extendedDateRangePickerToLabel": {}, + "extendedDateRangePickerWeekText": "{count, plural, zero{} one{Woche} other{Wochen}}", + "@extendedDateRangePickerWeekText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerYearText": "{count, plural, zero{} one{Jahr} other{Jahre}}", + "@extendedDateRangePickerYearText": { + "placeholders": { + "count": {} + } + }, "genericActionCancelLabel": "Abbrechen", "@genericActionCancelLabel": {}, "genericActionCreateLabel": "Erstellen", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index bb4a13d..92f41a5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -78,18 +78,6 @@ "@documentFilterAdvancedLabel": {}, "documentFilterApplyFilterLabel": "Apply", "@documentFilterApplyFilterLabel": {}, - "documentFilterDateRangeFieldEndLabel": "To", - "@documentFilterDateRangeFieldEndLabel": {}, - "documentFilterDateRangeFieldStartLabel": "From", - "@documentFilterDateRangeFieldStartLabel": {}, - "documentFilterDateRangeLastMonthLabel": "Last Month", - "@documentFilterDateRangeLastMonthLabel": {}, - "documentFilterDateRangeLastSevenDaysLabel": "Last 7 Days", - "@documentFilterDateRangeLastSevenDaysLabel": {}, - "documentFilterDateRangeLastThreeMonthsLabel": "Last 3 Months", - "@documentFilterDateRangeLastThreeMonthsLabel": {}, - "documentFilterDateRangeLastYearLabel": "Last Year", - "@documentFilterDateRangeLastYearLabel": {}, "documentFilterQueryOptionsAsnLabel": "ASN", "@documentFilterQueryOptionsAsnLabel": {}, "documentFilterQueryOptionsExtendedLabel": "Extended", @@ -272,6 +260,64 @@ "@errorMessageUnsupportedFileFormat": {}, "errorReportLabel": "REPORT", "@errorReportLabel": {}, + "extendedDateRangePickerAfterLabel": "After", + "@extendedDateRangePickerAfterLabel": {}, + "extendedDateRangePickerBeforeLabel": "Before", + "@extendedDateRangePickerBeforeLabel": {}, + "extendedDateRangePickerDayText": "{count, plural, zero{} one{day} other{days}}", + "@extendedDateRangePickerDayText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerFromLabel": "From", + "@extendedDateRangePickerFromLabel": {}, + "extendedDateRangePickerLastDaysLabel": "{count, plural, zero{} one{Yesterday} other{Last {count} days}}", + "@extendedDateRangePickerLastDaysLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastMonthsLabel": "{count, plural, zero{} one{Last month} other{Last {count} months}}", + "@extendedDateRangePickerLastMonthsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastText": "Last", + "@extendedDateRangePickerLastText": {}, + "extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}", + "@extendedDateRangePickerLastWeeksLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerLastYearsLabel": "{count, plural, zero{} one{Last year} other{Last {count} years}}", + "@extendedDateRangePickerLastYearsLabel": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}", + "@extendedDateRangePickerMonthText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerToLabel": "To", + "@extendedDateRangePickerToLabel": {}, + "extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}", + "@extendedDateRangePickerWeekText": { + "placeholders": { + "count": {} + } + }, + "extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}", + "@extendedDateRangePickerYearText": { + "placeholders": { + "count": {} + } + }, "genericActionCancelLabel": "Cancel", "@genericActionCancelLabel": {}, "genericActionCreateLabel": "Create", diff --git a/lib/main.dart b/lib/main.dart index 94e37c7..3e21d6b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index d446612..463f8eb 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -1,14 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:paperless_api/src/constants.dart'; -import 'package:paperless_api/src/models/query_parameters/asn_query.dart'; -import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; -import 'package:paperless_api/src/models/query_parameters/date_range_query.dart'; -import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; -import 'package:paperless_api/src/models/query_parameters/query_type.dart'; -import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; -import 'package:paperless_api/src/models/query_parameters/sort_order.dart'; -import 'package:paperless_api/src/models/query_parameters/storage_path_query.dart'; -import 'package:paperless_api/src/models/query_parameters/tags_query.dart'; +import 'package:paperless_api/paperless_api.dart'; class DocumentFilter extends Equatable { static const _oneDay = Duration(days: 1); @@ -23,23 +14,24 @@ class DocumentFilter extends Equatable { final int pageSize; final int page; - final DocumentTypeQuery documentType; - final CorrespondentQuery correspondent; - final StoragePathQuery storagePath; - final AsnQuery asnQuery; + final IdQueryParameter documentType; + final IdQueryParameter correspondent; + final IdQueryParameter storagePath; + final IdQueryParameter asnQuery; final TagsQuery tags; final SortField sortField; final SortOrder sortOrder; - final DateRangeQuery added; final DateRangeQuery created; + final DateRangeQuery added; + final DateRangeQuery modified; final QueryType queryType; final String? queryText; const DocumentFilter({ - this.documentType = const DocumentTypeQuery.unset(), - this.correspondent = const CorrespondentQuery.unset(), - this.storagePath = const StoragePathQuery.unset(), - this.asnQuery = const AsnQuery.unset(), + this.documentType = const IdQueryParameter.unset(), + this.correspondent = const IdQueryParameter.unset(), + this.storagePath = const IdQueryParameter.unset(), + this.asnQuery = const IdQueryParameter.unset(), this.tags = const IdsTagsQuery(), this.sortField = SortField.created, this.sortOrder = SortOrder.descending, @@ -49,6 +41,7 @@ class DocumentFilter extends Equatable { this.queryText, this.added = const UnsetDateRangeQuery(), this.created = const UnsetDateRangeQuery(), + this.modified = const UnsetDateRangeQuery(), }); Map toQueryParameters() { @@ -57,13 +50,14 @@ class DocumentFilter extends Equatable { 'page_size': pageSize.toString(), }; - params.addAll(documentType.toQueryParameter()); - params.addAll(correspondent.toQueryParameter()); + params.addAll(documentType.toQueryParameter('document_type')); + params.addAll(correspondent.toQueryParameter('correspondent')); + params.addAll(storagePath.toQueryParameter('storage_path')); + params.addAll(asnQuery.toQueryParameter('archive_serial_number')); params.addAll(tags.toQueryParameter()); - params.addAll(storagePath.toQueryParameter()); - params.addAll(asnQuery.toQueryParameter()); - params.addAll(added.toQueryParameter()); - params.addAll(created.toQueryParameter()); + params.addAll(added.toQueryParameter(DateRangeQueryField.added)); + params.addAll(created.toQueryParameter(DateRangeQueryField.created)); + params.addAll(modified.toQueryParameter(DateRangeQueryField.modified)); //TODO: Rework when implementing extended queries. if (queryText?.isNotEmpty ?? false) { params.putIfAbsent(queryType.queryParam, () => queryText!); @@ -84,15 +78,16 @@ class DocumentFilter extends Equatable { int? pageSize, int? page, bool? onlyNoDocumentType, - DocumentTypeQuery? documentType, - CorrespondentQuery? correspondent, - StoragePathQuery? storagePath, - AsnQuery? asnQuery, + IdQueryParameter? documentType, + IdQueryParameter? correspondent, + IdQueryParameter? storagePath, + IdQueryParameter? asnQuery, TagsQuery? tags, SortField? sortField, SortOrder? sortOrder, DateRangeQuery? added, DateRangeQuery? created, + DateRangeQuery? modified, QueryType? queryType, String? queryText, }) { @@ -105,11 +100,12 @@ class DocumentFilter extends Equatable { tags: tags ?? this.tags, sortField: sortField ?? this.sortField, sortOrder: sortOrder ?? this.sortOrder, - added: added ?? this.added, queryType: queryType ?? this.queryType, queryText: queryText ?? this.queryText, asnQuery: asnQuery ?? this.asnQuery, + added: added ?? this.added, created: created ?? this.created, + modified: modified ?? this.modified, ); } @@ -139,8 +135,9 @@ class DocumentFilter extends Equatable { correspondent != initial.correspondent, storagePath != initial.storagePath, tags != initial.tags, - (added != initial.added), - (created != initial.created), + added != initial.added, + created != initial.created, + modified != initial.modified, asnQuery != initial.asnQuery, (queryType != initial.queryType || queryText != initial.queryText), ].fold(0, (previousValue, element) => previousValue += element ? 1 : 0); @@ -158,6 +155,7 @@ class DocumentFilter extends Equatable { sortOrder, added, created, + modified, queryType, queryText, ]; diff --git a/packages/paperless_api/lib/src/models/filter_rule_model.dart b/packages/paperless_api/lib/src/models/filter_rule_model.dart index 02eb374..edc34fa 100644 --- a/packages/paperless_api/lib/src/models/filter_rule_model.dart +++ b/packages/paperless_api/lib/src/models/filter_rule_model.dart @@ -1,12 +1,6 @@ import 'package:equatable/equatable.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; -import 'package:paperless_api/src/models/document_filter.dart'; -import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; -import 'package:paperless_api/src/models/query_parameters/date_range_query.dart'; -import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; -import 'package:paperless_api/src/models/query_parameters/query_type.dart'; -import 'package:paperless_api/src/models/query_parameters/storage_path_query.dart'; -import 'package:paperless_api/src/models/query_parameters/tags_query.dart'; class FilterRule with EquatableMixin { static const int titleRule = 0; @@ -57,9 +51,6 @@ class FilterRule with EquatableMixin { } DocumentFilter applyToFilter(final DocumentFilter filter) { - if (value == null) { - return filter; - } //TODO: Check in profiling mode if this is inefficient enough to cause stutters... switch (ruleType) { case titleRule: @@ -67,20 +58,20 @@ class FilterRule with EquatableMixin { case documentTypeRule: return filter.copyWith( documentType: value == null - ? const DocumentTypeQuery.notAssigned() - : DocumentTypeQuery.fromId(int.parse(value!)), + ? const IdQueryParameter.notAssigned() + : IdQueryParameter.fromId(int.parse(value!)), ); case correspondentRule: return filter.copyWith( correspondent: value == null - ? const CorrespondentQuery.notAssigned() - : CorrespondentQuery.fromId(int.parse(value!)), + ? const IdQueryParameter.notAssigned() + : IdQueryParameter.fromId(int.parse(value!)), ); case storagePathRule: return filter.copyWith( storagePath: value == null - ? const StoragePathQuery.notAssigned() - : StoragePathQuery.fromId(int.parse(value!)), + ? const IdQueryParameter.notAssigned() + : IdQueryParameter.fromId(int.parse(value!)), ); case hasAnyTag: return filter.copyWith( @@ -101,48 +92,69 @@ class FilterRule with EquatableMixin { .withIdQueriesAdded([ExcludeTagIdQuery(int.parse(value!))]), ); case createdBeforeRule: - if (filter.created is FixedDateRangeQuery) { + if (filter.created is AbsoluteDateRangeQuery) { return filter.copyWith( - created: (filter.created as FixedDateRangeQuery) + created: (filter.created as AbsoluteDateRangeQuery) .copyWith(before: DateTime.parse(value!)), ); } else { return filter.copyWith( - created: - FixedDateRangeQuery.created(before: DateTime.parse(value!)), + created: AbsoluteDateRangeQuery(before: DateTime.parse(value!)), ); } case createdAfterRule: - if (filter.created is FixedDateRangeQuery) { + if (filter.created is AbsoluteDateRangeQuery) { return filter.copyWith( - created: (filter.created as FixedDateRangeQuery) + created: (filter.created as AbsoluteDateRangeQuery) .copyWith(after: DateTime.parse(value!)), ); } else { return filter.copyWith( - created: FixedDateRangeQuery.created(after: DateTime.parse(value!)), + created: AbsoluteDateRangeQuery(after: DateTime.parse(value!)), ); } case addedBeforeRule: - if (filter.added is FixedDateRangeQuery) { + if (filter.added is AbsoluteDateRangeQuery) { return filter.copyWith( - added: (filter.added as FixedDateRangeQuery) + added: (filter.added as AbsoluteDateRangeQuery) .copyWith(before: DateTime.parse(value!)), ); } else { return filter.copyWith( - added: FixedDateRangeQuery.added(before: DateTime.parse(value!)), + added: AbsoluteDateRangeQuery(before: DateTime.parse(value!)), ); } case addedAfterRule: - if (filter.added is FixedDateRangeQuery) { + if (filter.added is AbsoluteDateRangeQuery) { return filter.copyWith( - added: (filter.added as FixedDateRangeQuery) + added: (filter.added as AbsoluteDateRangeQuery) .copyWith(after: DateTime.parse(value!)), ); } else { return filter.copyWith( - added: FixedDateRangeQuery.added(after: DateTime.parse(value!)), + added: AbsoluteDateRangeQuery(after: DateTime.parse(value!)), + ); + } + case modifiedBeforeRule: + if (filter.modified is AbsoluteDateRangeQuery) { + return filter.copyWith( + modified: (filter.modified as AbsoluteDateRangeQuery) + .copyWith(before: DateTime.parse(value!)), + ); + } else { + return filter.copyWith( + modified: AbsoluteDateRangeQuery(before: DateTime.parse(value!)), + ); + } + case modifiedAfterRule: + if (filter.modified is AbsoluteDateRangeQuery) { + return filter.copyWith( + modified: (filter.modified as AbsoluteDateRangeQuery) + .copyWith(after: DateTime.parse(value!)), + ); + } else { + return filter.copyWith( + added: AbsoluteDateRangeQuery(after: DateTime.parse(value!)), ); } case titleAndContentRule: @@ -171,7 +183,7 @@ class FilterRule with EquatableMixin { switch (field) { case 'created': newFilter = newFilter.copyWith( - created: LastNDateRangeQuery.created( + created: RelativeDateRangeQuery( n, DateRangeUnit.values.byName(unit), ), @@ -179,7 +191,7 @@ class FilterRule with EquatableMixin { break; case 'added': newFilter = newFilter.copyWith( - created: LastNDateRangeQuery.added( + added: RelativeDateRangeQuery( n, DateRangeUnit.values.byName(unit), ), @@ -187,7 +199,7 @@ class FilterRule with EquatableMixin { break; case 'modified': newFilter = newFilter.copyWith( - created: LastNDateRangeQuery.modified( + modified: RelativeDateRangeQuery( n, DateRangeUnit.values.byName(unit), ), @@ -262,7 +274,7 @@ class FilterRule with EquatableMixin { // Parse created at final created = filter.created; - if (created is FixedDateRangeQuery) { + if (created is AbsoluteDateRangeQuery) { if (created.after != null) { filterRules.add( FilterRule(createdAfterRule, apiDateFormat.format(created.after!)), @@ -273,15 +285,16 @@ class FilterRule with EquatableMixin { FilterRule(createdBeforeRule, apiDateFormat.format(created.before!)), ); } - } else if (created is LastNDateRangeQuery) { + } else if (created is RelativeDateRangeQuery) { filterRules.add( - FilterRule(extendedRule, created.toQueryParameter().values.first), + FilterRule(extendedRule, + created.toQueryParameter(DateRangeQueryField.created).values.first), ); } // Parse added at final added = filter.added; - if (added is FixedDateRangeQuery) { + if (added is AbsoluteDateRangeQuery) { if (added.after != null) { filterRules.add( FilterRule(addedAfterRule, apiDateFormat.format(added.after!)), @@ -292,15 +305,16 @@ class FilterRule with EquatableMixin { FilterRule(addedBeforeRule, apiDateFormat.format(added.before!)), ); } - } else if (added is LastNDateRangeQuery) { + } else if (added is RelativeDateRangeQuery) { filterRules.add( - FilterRule(extendedRule, added.toQueryParameter().values.first), + FilterRule(extendedRule, + added.toQueryParameter(DateRangeQueryField.added).values.first), ); } // Parse modified at - final modified = filter.added; - if (modified is FixedDateRangeQuery) { + final modified = filter.modified; + if (modified is AbsoluteDateRangeQuery) { if (modified.after != null) { filterRules.add( FilterRule(modifiedAfterRule, apiDateFormat.format(modified.after!)), @@ -312,9 +326,14 @@ class FilterRule with EquatableMixin { modifiedBeforeRule, apiDateFormat.format(modified.before!)), ); } - } else if (modified is LastNDateRangeQuery) { + } else if (modified is RelativeDateRangeQuery) { filterRules.add( - FilterRule(extendedRule, modified.toQueryParameter().values.first), + FilterRule( + extendedRule, + modified + .toQueryParameter(DateRangeQueryField.modified) + .values + .first), ); } diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index 12e48f7..7e90907 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -4,15 +4,12 @@ export 'labels/label_model.dart'; export 'labels/matching_algorithm.dart'; export 'labels/storage_path_model.dart'; export 'labels/tag_model.dart'; -export 'query_parameters/asn_query.dart'; -export 'query_parameters/correspondent_query.dart'; -export 'query_parameters/document_type_query.dart'; export 'query_parameters/id_query_parameter.dart'; export 'query_parameters/query_type.dart'; export 'query_parameters/sort_field.dart'; export 'query_parameters/sort_order.dart'; -export 'query_parameters/storage_path_query.dart'; export 'query_parameters/tags_query.dart'; +export 'query_parameters/date_range_query.dart'; export 'bulk_edit_model.dart'; export 'document_filter.dart'; export 'document_meta_data_model.dart'; diff --git a/packages/paperless_api/lib/src/models/query_parameters/asn_query.dart b/packages/paperless_api/lib/src/models/query_parameters/asn_query.dart deleted file mode 100644 index 93b890b..0000000 --- a/packages/paperless_api/lib/src/models/query_parameters/asn_query.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:paperless_api/src/models/query_parameters/id_query_parameter.dart'; - -class AsnQuery extends IdQueryParameter { - const AsnQuery.fromId(super.id) : super.fromId(); - const AsnQuery.unset() : super.unset(); - const AsnQuery.notAssigned() : super.notAssigned(); - const AsnQuery.anyAssigned() : super.anyAssigned(); - - @override - String get queryParameterKey => 'archive_serial_number'; -} diff --git a/packages/paperless_api/lib/src/models/query_parameters/correspondent_query.dart b/packages/paperless_api/lib/src/models/query_parameters/correspondent_query.dart deleted file mode 100644 index f64b2e1..0000000 --- a/packages/paperless_api/lib/src/models/query_parameters/correspondent_query.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:paperless_api/src/models/query_parameters/id_query_parameter.dart'; - -class CorrespondentQuery extends IdQueryParameter { - const CorrespondentQuery.fromId(super.id) : super.fromId(); - const CorrespondentQuery.unset() : super.unset(); - const CorrespondentQuery.notAssigned() : super.notAssigned(); - const CorrespondentQuery.anyAssigned() : super.anyAssigned(); - - @override - String get queryParameterKey => 'correspondent'; -} diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart index 076c147..b0c997d 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart @@ -3,7 +3,7 @@ import 'package:paperless_api/src/constants.dart'; abstract class DateRangeQuery extends Equatable { const DateRangeQuery(); - Map toQueryParameter(); + Map toQueryParameter(DateRangeQueryField field); } class UnsetDateRangeQuery extends DateRangeQuery { @@ -12,87 +12,74 @@ class UnsetDateRangeQuery extends DateRangeQuery { List get props => []; @override - Map toQueryParameter() => const {}; + Map toQueryParameter(DateRangeQueryField field) => const {}; } -class FixedDateRangeQuery extends DateRangeQuery { - final String _querySuffix; - +class AbsoluteDateRangeQuery extends DateRangeQuery { final DateTime? after; final DateTime? before; - const FixedDateRangeQuery._(this._querySuffix, {this.after, this.before}) - : assert(after != null || before != null); - - const FixedDateRangeQuery.created({DateTime? after, DateTime? before}) - : this._('created', after: after, before: before); - - const FixedDateRangeQuery.added({DateTime? after, DateTime? before}) - : this._('added', after: after, before: before); - - const FixedDateRangeQuery.modified({DateTime? after, DateTime? before}) - : this._('modified', after: after, before: before); + const AbsoluteDateRangeQuery({this.after, this.before}); @override - List get props => [_querySuffix, after, before]; + List get props => [after, before]; @override - Map toQueryParameter() { + Map toQueryParameter(DateRangeQueryField field) { final Map params = {}; // Add/subtract one day in the following because paperless uses gt/lt not gte/lte if (after != null) { - params.putIfAbsent('${_querySuffix}__date__gt', + params.putIfAbsent('${field.name}__date__gt', () => apiDateFormat.format(after!.subtract(const Duration(days: 1)))); } if (before != null) { - params.putIfAbsent('${_querySuffix}__date__lt', + params.putIfAbsent('${field.name}__date__lt', () => apiDateFormat.format(before!.add(const Duration(days: 1)))); } return params; } - FixedDateRangeQuery copyWith({ + AbsoluteDateRangeQuery copyWith({ DateTime? before, DateTime? after, }) { - return FixedDateRangeQuery._( - _querySuffix, + return AbsoluteDateRangeQuery( before: before ?? this.before, after: after ?? this.after, ); } } -class LastNDateRangeQuery extends DateRangeQuery { +class RelativeDateRangeQuery extends DateRangeQuery { + final int offset; final DateRangeUnit unit; - final int n; - final String _field; - const LastNDateRangeQuery._( - this._field, { - required this.n, - required this.unit, - }); - - const LastNDateRangeQuery.created(int n, DateRangeUnit unit) - : this._('created', unit: unit, n: n); - const LastNDateRangeQuery.added(int n, DateRangeUnit unit) - : this._('added', unit: unit, n: n); - const LastNDateRangeQuery.modified(int n, DateRangeUnit unit) - : this._('modified', unit: unit, n: n); + const RelativeDateRangeQuery( + this.offset, + this.unit, + ); @override - // TODO: implement props - List get props => [_field, n, unit]; + List get props => [offset, unit]; @override - Map toQueryParameter() { + Map toQueryParameter(DateRangeQueryField field) { return { - 'query': '[$_field:$n ${unit.name} to now]', + 'query': '[${field.name}:$offset ${unit.name} to now]', }; } + + RelativeDateRangeQuery copyWith({ + int? offset, + DateRangeUnit? unit, + }) { + return RelativeDateRangeQuery( + offset ?? this.offset, + unit ?? this.unit, + ); + } } enum DateRangeUnit { @@ -101,3 +88,9 @@ enum DateRangeUnit { month, year; } + +enum DateRangeQueryField { + created, + added, + modified; +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/document_type_query.dart b/packages/paperless_api/lib/src/models/query_parameters/document_type_query.dart deleted file mode 100644 index ca6a009..0000000 --- a/packages/paperless_api/lib/src/models/query_parameters/document_type_query.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:paperless_api/src/models/query_parameters/id_query_parameter.dart'; - -class DocumentTypeQuery extends IdQueryParameter { - const DocumentTypeQuery.fromId(super.id) : super.fromId(); - const DocumentTypeQuery.unset() : super.unset(); - const DocumentTypeQuery.notAssigned() : super.notAssigned(); - const DocumentTypeQuery.anyAssigned() : super.anyAssigned(); - - @override - String get queryParameterKey => 'document_type'; -} diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart index 610e1e5..2fac2a8 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; -abstract class IdQueryParameter extends Equatable { +class IdQueryParameter extends Equatable { final int? _assignmentStatus; final int? _id; @@ -28,16 +28,14 @@ abstract class IdQueryParameter extends Equatable { int? get id => _id; - String get queryParameterKey; - - Map toQueryParameter() { + Map toQueryParameter(String field) { final Map params = {}; if (onlyNotAssigned || onlyAssigned) { params.putIfAbsent( - '${queryParameterKey}__isnull', () => _assignmentStatus!.toString()); + '${field}__isnull', () => _assignmentStatus!.toString()); } if (isSet) { - params.putIfAbsent("${queryParameterKey}__id", () => id!.toString()); + params.putIfAbsent("${field}__id", () => id!.toString()); } return params; } diff --git a/packages/paperless_api/lib/src/models/query_parameters/storage_path_query.dart b/packages/paperless_api/lib/src/models/query_parameters/storage_path_query.dart deleted file mode 100644 index e0ccc98..0000000 --- a/packages/paperless_api/lib/src/models/query_parameters/storage_path_query.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:paperless_api/src/models/query_parameters/id_query_parameter.dart'; - -class StoragePathQuery extends IdQueryParameter { - const StoragePathQuery.fromId(super.id) : super.fromId(); - const StoragePathQuery.unset() : super.unset(); - const StoragePathQuery.notAssigned() : super.notAssigned(); - const StoragePathQuery.anyAssigned() : super.anyAssigned(); - - @override - String get queryParameterKey => 'storage_path'; -} diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index b1b2ee0..a18ef92 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:http/src/boundary_characters.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; import 'package:paperless_api/src/models/bulk_edit_model.dart'; import 'package:paperless_api/src/models/document_filter.dart'; @@ -11,7 +12,6 @@ import 'package:paperless_api/src/models/document_meta_data_model.dart'; import 'package:paperless_api/src/models/document_model.dart'; import 'package:paperless_api/src/models/paged_search_result.dart'; import 'package:paperless_api/src/models/paperless_server_exception.dart'; -import 'package:paperless_api/src/models/query_parameters/asn_query.dart'; import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; import 'package:paperless_api/src/models/query_parameters/sort_order.dart'; import 'package:paperless_api/src/models/similar_document_model.dart'; @@ -139,7 +139,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { final filterParams = filter.toQueryParameters(); final response = await baseClient.get( Uri( - path: "/api/documents/?$filterParams", + path: "/api/documents/", queryParameters: filterParams, ), ); @@ -190,7 +190,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { const DocumentFilter asnQueryFilter = DocumentFilter( sortField: SortField.archiveSerialNumber, sortOrder: SortOrder.descending, - asnQuery: AsnQuery.anyAssigned(), + asnQuery: IdQueryParameter.anyAssigned(), page: 1, pageSize: 1, ); diff --git a/packages/paperless_api/test/saved_view_test.dart b/packages/paperless_api/test/saved_view_test.dart index 38d6511..6a175f7 100644 --- a/packages/paperless_api/test/saved_view_test.dart +++ b/packages/paperless_api/test/saved_view_test.dart @@ -1,14 +1,5 @@ -import 'package:paperless_api/src/models/document_filter.dart'; -import 'package:paperless_api/src/models/filter_rule_model.dart'; -import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; -import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; -import 'package:paperless_api/src/models/query_parameters/query_type.dart'; -import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; -import 'package:paperless_api/src/models/query_parameters/sort_order.dart'; -import 'package:paperless_api/src/models/query_parameters/storage_path_query.dart'; -import 'package:paperless_api/src/models/query_parameters/tags_query.dart'; -import 'package:paperless_api/src/models/saved_view_model.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:paperless_api/paperless_api.dart'; void main() { group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () { @@ -74,10 +65,10 @@ void main() { }).toDocumentFilter(), equals( DocumentFilter.initial.copyWith( - correspondent: const CorrespondentQuery.fromId(42), - documentType: const DocumentTypeQuery.fromId(69), - storagePath: const StoragePathQuery.fromId(14), - tags: IdsTagsQuery( + correspondent: const IdQueryParameter.fromId(42), + documentType: const IdQueryParameter.fromId(69), + storagePath: const IdQueryParameter.fromId(14), + tags: const IdsTagsQuery( [ IncludeTagIdQuery(1), IncludeTagIdQuery(2), @@ -85,10 +76,14 @@ void main() { ExcludeTagIdQuery(4), ], ), - createdDateBefore: DateTime.parse("2022-10-27"), - createdDateAfter: DateTime.parse("2022-09-27"), - addedDateBefore: DateTime.parse("2022-09-26"), - addedDateAfter: DateTime.parse("2000-01-01"), + created: AbsoluteDateRangeQuery( + before: DateTime.parse("2022-10-27"), + after: DateTime.parse("2022-09-27"), + ), + added: AbsoluteDateRangeQuery( + before: DateTime.parse("2022-09-26"), + after: DateTime.parse("2000-01-01"), + ), sortField: SortField.created, sortOrder: SortOrder.descending, queryText: "Never gonna give you up", @@ -114,39 +109,41 @@ void main() { }); test('Values are correctly parsed if not assigned.', () { + final actual = SavedView.fromJson({ + "id": 1, + "name": "test_name", + "show_on_dashboard": false, + "show_in_sidebar": false, + "sort_field": SortField.created.name, + "sort_reverse": true, + "filter_rules": [ + { + 'rule_type': FilterRule.correspondentRule, + 'value': null, + }, + { + 'rule_type': FilterRule.documentTypeRule, + 'value': null, + }, + { + 'rule_type': FilterRule.hasAnyTag, + 'value': false.toString(), + }, + { + 'rule_type': FilterRule.storagePathRule, + 'value': null, + }, + ], + }).toDocumentFilter(); + final expected = DocumentFilter.initial.copyWith( + correspondent: const IdQueryParameter.notAssigned(), + documentType: const IdQueryParameter.notAssigned(), + storagePath: const IdQueryParameter.notAssigned(), + tags: const OnlyNotAssignedTagsQuery(), + ); expect( - SavedView.fromJson({ - "id": 1, - "name": "test_name", - "show_on_dashboard": false, - "show_in_sidebar": false, - "sort_field": SortField.created.name, - "sort_reverse": true, - "filter_rules": [ - { - 'rule_type': FilterRule.correspondentRule, - 'value': null, - }, - { - 'rule_type': FilterRule.documentTypeRule, - 'value': null, - }, - { - 'rule_type': FilterRule.hasAnyTag, - 'value': false.toString(), - }, - { - 'rule_type': FilterRule.storagePathRule, - 'value': null, - }, - ], - }).toDocumentFilter(), - equals(DocumentFilter.initial.copyWith( - correspondent: const CorrespondentQuery.notAssigned(), - documentType: const DocumentTypeQuery.notAssigned(), - storagePath: const StoragePathQuery.notAssigned(), - tags: const OnlyNotAssignedTagsQuery(), - )), + actual, + equals(expected), ); }); }); @@ -156,10 +153,10 @@ void main() { expect( SavedView.fromDocumentFilter( DocumentFilter( - correspondent: const CorrespondentQuery.fromId(1), - documentType: const DocumentTypeQuery.fromId(2), - storagePath: const StoragePathQuery.fromId(3), - tags: IdsTagsQuery([ + correspondent: const IdQueryParameter.fromId(1), + documentType: const IdQueryParameter.fromId(2), + storagePath: const IdQueryParameter.fromId(3), + tags: const IdsTagsQuery([ IncludeTagIdQuery(4), IncludeTagIdQuery(5), ExcludeTagIdQuery(6), @@ -168,10 +165,14 @@ void main() { ]), sortField: SortField.added, sortOrder: SortOrder.ascending, - addedDateAfter: DateTime.parse("2020-01-01"), - addedDateBefore: DateTime.parse("2020-03-01"), - createdDateAfter: DateTime.parse("2020-02-01"), - createdDateBefore: DateTime.parse("2020-04-01"), + created: AbsoluteDateRangeQuery( + before: DateTime.parse("2020-04-01"), + after: DateTime.parse("2020-02-01"), + ), + added: AbsoluteDateRangeQuery( + before: DateTime.parse("2020-03-01"), + after: DateTime.parse("2020-01-01"), + ), queryText: "Never gonna let you down", queryType: QueryType.title, ), @@ -210,16 +211,14 @@ void main() { expect( SavedView.fromDocumentFilter( const DocumentFilter( - correspondent: CorrespondentQuery.unset(), - documentType: DocumentTypeQuery.unset(), - storagePath: StoragePathQuery.unset(), + correspondent: IdQueryParameter.unset(), + documentType: IdQueryParameter.unset(), + storagePath: IdQueryParameter.unset(), tags: IdsTagsQuery(), sortField: SortField.created, sortOrder: SortOrder.descending, - addedDateAfter: null, - addedDateBefore: null, - createdDateAfter: null, - createdDateBefore: null, + added: UnsetDateRangeQuery(), + created: UnsetDateRangeQuery(), queryText: null, ), name: "test_name", @@ -243,9 +242,9 @@ void main() { expect( SavedView.fromDocumentFilter( const DocumentFilter( - correspondent: CorrespondentQuery.notAssigned(), - documentType: DocumentTypeQuery.notAssigned(), - storagePath: StoragePathQuery.notAssigned(), + correspondent: IdQueryParameter.notAssigned(), + documentType: IdQueryParameter.notAssigned(), + storagePath: IdQueryParameter.notAssigned(), tags: OnlyNotAssignedTagsQuery(), sortField: SortField.created, sortOrder: SortOrder.ascending,