diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 184acd4..d963ee2 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -100,12 +100,19 @@ class DocumentsCubit extends Cubit { /// Update filter state and automatically reload documents. Always resets page to 1. /// Use [DocumentsCubit.loadMore] to load more data. Future updateFilter({ - DocumentFilter filter = DocumentFilter.initial, + final DocumentFilter filter = DocumentFilter.initial, }) async { final result = await documentRepository.find(filter.copyWith(page: 1)); emit(DocumentsState(filter: filter, value: [result], isLoaded: true)); } + /// + /// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter. + /// + Future updateCurrentFilter(final DocumentFilter Function(DocumentFilter) transformFn) { + return updateFilter(filter: transformFn(state.filter)); + } + void toggleDocumentSelection(DocumentModel model) { if (state.selection.contains(model)) { emit( diff --git a/lib/features/documents/model/document_filter.dart b/lib/features/documents/model/document_filter.dart index 5180d8b..c288120 100644 --- a/lib/features/documents/model/document_filter.dart +++ b/lib/features/documents/model/document_filter.dart @@ -10,6 +10,7 @@ import 'package:flutter_paperless_mobile/features/documents/model/query_paramete import 'package:flutter_paperless_mobile/util.dart'; class DocumentFilter with EquatableMixin { + static const _oneDay = Duration(days: 1); static const DocumentFilter initial = DocumentFilter(); static const DocumentFilter latestDocument = DocumentFilter( @@ -67,20 +68,21 @@ class DocumentFilter with EquatableMixin { sb.write("&ordering=${sortOrder.queryString}${sortField.queryString}"); + // Add/subtract one day in the following because paperless uses gt/lt not gte/lte if (addedDateAfter != null) { - sb.write("&added__date__gt=${dateFormat.format(addedDateAfter!)}"); + sb.write("&added__date__gt=${dateFormat.format(addedDateAfter!.subtract(_oneDay))}"); } if (addedDateBefore != null) { - sb.write("&added__date__lt=${dateFormat.format(addedDateBefore!)}"); + sb.write("&added__date__lt=${dateFormat.format(addedDateBefore!.add(_oneDay))}"); } if (createdDateAfter != null) { - sb.write("&created__date__gt=${dateFormat.format(createdDateAfter!)}"); + sb.write("&created__date__gt=${dateFormat.format(createdDateAfter!.subtract(_oneDay))}"); } if (createdDateBefore != null) { - sb.write("&created__date__lt=${dateFormat.format(createdDateBefore!)}"); + sb.write("&created__date__lt=${dateFormat.format(createdDateBefore!.add(_oneDay))}"); } return sb.toString(); diff --git a/lib/features/documents/model/query_parameters/id_query_parameter.dart b/lib/features/documents/model/query_parameters/id_query_parameter.dart index 8605d94..6274a87 100644 --- a/lib/features/documents/model/query_parameters/id_query_parameter.dart +++ b/lib/features/documents/model/query_parameters/id_query_parameter.dart @@ -37,7 +37,7 @@ abstract class IdQueryParameter extends Equatable { return "&${queryParameterKey}__isnull=$_assignmentStatus"; } if (isSet) { - return "${queryParameterKey}__id=$id"; + return "&${queryParameterKey}__id=$id"; } return ""; } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 6263f03..8a50af3 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -122,9 +122,7 @@ class _DocumentsPageState extends State { controller: _panelController, defaultPanelState: PanelState.CLOSED, minHeight: 48, - maxHeight: MediaQuery.of(context).size.height - - kBottomNavigationBarHeight - - 2 * kToolbarHeight, + maxHeight: MediaQuery.of(context).size.height - kBottomNavigationBarHeight, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), 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 46884c7..643fb84 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -2,27 +2,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/sort_field.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/query_type.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart'; -import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; -import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:flutter_paperless_mobile/features/documents/bloc/saved_view_cubit.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart'; import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/query_type.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/sort_field.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:flutter_paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart'; +import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; +import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart'; import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:flutter_paperless_mobile/features/labels/view/widgets/label_form_field.dart'; -import 'package:flutter_paperless_mobile/features/scan/view/document_upload_page.dart'; import 'package:flutter_paperless_mobile/generated/l10n.dart'; import 'package:intl/intl.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; @@ -62,7 +61,14 @@ class _DocumentFilterPanelState extends State { ]; final _formKey = GlobalKey(); - bool _isQueryLoading = false; + + late final DocumentsCubit _documentsCubit; + + @override + void initState() { + super.initState(); + _documentsCubit = BlocProvider.of(context); + } DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) { if (start == null && end == null) { @@ -78,18 +84,21 @@ class _DocumentFilterPanelState extends State { @override Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - // Set initial values, otherwise they would not automatically update. - _patchFromFilter(state.filter); - }, - builder: (context, state) { - return FormBuilder( - key: _formKey, - child: MediaQuery.removePadding( - context: context, - removeTop: true, - child: Column( + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + child: BlocConsumer( + listener: (context, state) { + // Set initial values, otherwise they would not automatically update. + _patchFromFilter(state.filter); + }, + builder: (context, state) { + return FormBuilder( + key: _formKey, + child: ListView( + controller: widget.scrollController, children: [ Stack( alignment: Alignment.center, @@ -121,40 +130,37 @@ class _DocumentFilterPanelState extends State { ), ], ).padded(), - Expanded( - child: ListView( - controller: widget.scrollController, - children: [ - const SizedBox( - height: 16.0, - ), - Align( - alignment: Alignment.centerLeft, - child: Text(S.of(context).documentsFilterPageSearchLabel), - ).padded(), - _buildQueryFormField(state), - _buildSortByChipsList(context, state), - Align( - alignment: Alignment.centerLeft, - child: Text(S.of(context).documentsFilterPageAdvancedLabel), - ).padded(), - _buildCreatedDateRangePickerFormField(state).padded(), - _buildAddedDateRangePickerFormField(state).padded(), - _buildCorrespondentFormField(state).padded(), - _buildDocumentTypeFormField(state).padded(), - _buildStoragePathFormField(state).padded(), - TagFormField( - name: DocumentModel.tagsKey, - initialValue: state.filter.tags, - ).padded(), - ], - ), + const SizedBox( + height: 16.0, + ), + Align( + alignment: Alignment.centerLeft, + child: Text(S.of(context).documentsFilterPageSearchLabel), + ).padded(const EdgeInsets.only(left: 8.0)), + _buildQueryFormField(state), + _buildSortByChipsList(context, state), + Align( + alignment: Alignment.centerLeft, + child: Text(S.of(context).documentsFilterPageAdvancedLabel), + ).padded(const EdgeInsets.only(left: 8.0, top: 8.0)), + _buildCreatedDateRangePickerFormField(state).padded(), + _buildAddedDateRangePickerFormField(state).padded(), + _buildCorrespondentFormField(state).padded(), + _buildDocumentTypeFormField(state).padded(), + _buildStoragePathFormField(state).padded(), + TagFormField( + name: DocumentModel.tagsKey, + initialValue: state.filter.tags, + ).padded(), + // Required in order for the storage path field to be visible when typing + const SizedBox( + height: 200, ), ], ), - ), - ); - }, + ); + }, + ), ); } @@ -184,6 +190,23 @@ class _DocumentFilterPanelState extends State { ); } + Widget _buildCorrespondentFormField(DocumentsState docState) { + return BlocBuilder>( + builder: (context, state) { + return LabelFormField( + formBuilderState: _formKey.currentState, + name: fkCorrespondent, + state: state, + label: S.of(context).documentCorrespondentPropertyLabel, + initialValue: docState.filter.correspondent, + queryParameterIdBuilder: CorrespondentQuery.fromId, + queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, + prefixIcon: const Icon(Icons.person_outline), + ); + }, + ); + } + Widget _buildStoragePathFormField(DocumentsState docState) { return BlocBuilder>( builder: (context, state) { @@ -308,23 +331,6 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildCorrespondentFormField(DocumentsState docState) { - return BlocBuilder>( - builder: (context, state) { - return LabelFormField( - formBuilderState: _formKey.currentState, - name: fkCorrespondent, - state: state, - label: S.of(context).documentCorrespondentPropertyLabel, - initialValue: docState.filter.correspondent, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, - prefixIcon: const Icon(Icons.person_outline), - ); - }, - ); - } - Widget _buildCreatedDateRangePickerFormField(DocumentsState state) { return Column( children: [ @@ -333,16 +339,21 @@ class _DocumentFilterPanelState extends State { state.filter.createdDateAfter, state.filter.createdDateBefore, ), - pickerBuilder: (context, child) { - return Theme( - data: ThemeData.light().copyWith( - primaryColor: Theme.of(context).primaryColor, - colorScheme: Theme.of(context).colorScheme, - buttonTheme: Theme.of(context).buttonTheme, - ), - child: child!, - ); - }, + // 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).documentsFilterPageDateRangeFieldStartLabel, fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel, @@ -371,16 +382,21 @@ class _DocumentFilterPanelState extends State { state.filter.addedDateAfter, state.filter.addedDateBefore, ), - pickerBuilder: (context, child) { - return Theme( - data: ThemeData.light().copyWith( - primaryColor: Theme.of(context).primaryColor, - colorScheme: Theme.of(context).colorScheme, - buttonTheme: Theme.of(context).buttonTheme, - ), - child: child!, - ); - }, + // 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).documentsFilterPageDateRangeFieldStartLabel, fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel, @@ -413,30 +429,27 @@ class _DocumentFilterPanelState extends State { } Widget _buildSortByChipsList(BuildContext context, DocumentsState state) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).documentsPageOrderByLabel, - ), - SizedBox( - height: kToolbarHeight, - child: ListView.separated( - itemCount: _sortFields.length, - scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => const SizedBox( - width: 8.0, - ), - itemBuilder: (context, index) => - _buildActionChip(_sortFields[index], state.filter.sortField, context), + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).documentsPageOrderByLabel, + ), + SizedBox( + height: kToolbarHeight, + child: ListView.separated( + itemCount: _sortFields.length, + scrollDirection: Axis.horizontal, + separatorBuilder: (context, index) => const SizedBox( + width: 8.0, ), + itemBuilder: (context, index) => + _buildActionChip(_sortFields[index], state.filter.sortField, context), ), - ], - ), - ); + ), + ], + ).padded(); } Widget _buildActionChip( @@ -481,9 +494,7 @@ class _DocumentFilterPanelState extends State { } void _onApplyFilter() { - setState(() => _isQueryLoading = true); - _formKey.currentState?.save(); - if (_formKey.currentState?.validate() ?? false) { + if (_formKey.currentState?.saveAndValidate() ?? false) { final v = _formKey.currentState!.value; final docCubit = BlocProvider.of(context); DocumentFilter newFilter = docCubit.state.filter.copyWith( @@ -503,7 +514,6 @@ class _DocumentFilterPanelState extends State { BlocProvider.of(context).resetSelection(); FocusScope.of(context).unfocus(); widget.panelController.close(); - setState(() => _isQueryLoading = false); }); } } diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 4c89f88..02a2057 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -43,15 +43,14 @@ class CorrespondentWidget extends StatelessWidget { } void _addCorrespondentToFilter(BuildContext context) { - final cubit = getIt(); + final cubit = BlocProvider.of(context); if (cubit.state.filter.correspondent.id == correspondentId) { - cubit.updateFilter( - filter: cubit.state.filter.copyWith(correspondent: const CorrespondentQuery.unset())); + cubit.updateCurrentFilter( + (filter) => filter.copyWith(correspondent: const CorrespondentQuery.unset()), + ); } else { - cubit.updateFilter( - filter: cubit.state.filter.copyWith( - correspondent: CorrespondentQuery.fromId(correspondentId), - ), + cubit.updateCurrentFilter( + (filter) => filter.copyWith(correspondent: CorrespondentQuery.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 66acb35..31c160b 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 @@ -1,52 +1,53 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_paperless_mobile/di_initializer.dart'; +import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart'; import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart'; class DocumentTypeWidget extends StatelessWidget { final int? documentTypeId; final void Function()? afterSelected; + final bool isSelectable; const DocumentTypeWidget({ Key? key, required this.documentTypeId, this.afterSelected, + this.isSelectable = true, }) : super(key: key); @override Widget build(BuildContext context) { - return GestureDetector( - onTap: _addDocumentTypeToFilter, - child: BlocBuilder>( - builder: (context, state) { - return Text( - state[documentTypeId]?.toString() ?? "-", - style: Theme.of(context) - .textTheme - .bodyText2! - .copyWith(color: Theme.of(context).colorScheme.primary), - ); - }, + return AbsorbPointer( + absorbing: !isSelectable, + child: GestureDetector( + onTap: () => _addDocumentTypeToFilter(context), + child: BlocBuilder>( + builder: (context, state) { + return Text( + state[documentTypeId]?.toString() ?? "-", + style: Theme.of(context) + .textTheme + .bodyText2! + .copyWith(color: Theme.of(context).colorScheme.tertiary), + ); + }, + ), ), ); } - void _addDocumentTypeToFilter() { - final cubit = getIt(); + void _addDocumentTypeToFilter(BuildContext context) { + final cubit = BlocProvider.of(context); if (cubit.state.filter.documentType.id == documentTypeId) { - cubit.updateFilter( - filter: cubit.state.filter.copyWith(documentType: const DocumentTypeQuery.unset())); + cubit.updateCurrentFilter( + (filter) => filter.copyWith(documentType: const DocumentTypeQuery.unset()), + ); } else { - cubit.updateFilter( - filter: cubit.state.filter.copyWith( - documentType: DocumentTypeQuery.fromId(documentTypeId), - ), + cubit.updateCurrentFilter( + (filter) => filter.copyWith(documentType: DocumentTypeQuery.fromId(documentTypeId)), ); } - if (afterSelected != null) { - afterSelected?.call(); - } + 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 8ebdec7..de84646 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 @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_paperless_mobile/di_initializer.dart'; import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart'; import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; @@ -43,15 +42,14 @@ class StoragePathWidget extends StatelessWidget { } void _addStoragePathToFilter(BuildContext context) { - final cubit = getIt(); + final cubit = BlocProvider.of(context); if (cubit.state.filter.correspondent.id == pathId) { - cubit.updateFilter( - filter: cubit.state.filter.copyWith(storagePath: const StoragePathQuery.unset())); + cubit.updateCurrentFilter( + (filter) => filter.copyWith(storagePath: const StoragePathQuery.unset()), + ); } else { - cubit.updateFilter( - filter: cubit.state.filter.copyWith( - storagePath: StoragePathQuery.fromId(pathId), - ), + cubit.updateCurrentFilter( + (filter) => filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)), ); } afterSelected?.call(); diff --git a/lib/features/labels/tags/view/pages/edit_tag_page.dart b/lib/features/labels/tags/view/pages/edit_tag_page.dart index 3ff592f..a0360ee 100644 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ b/lib/features/labels/tags/view/pages/edit_tag_page.dart @@ -50,12 +50,16 @@ class EditTagPage extends StatelessWidget { late DocumentFilter updatedFilter = currentFilter; if (currentFilter.tags.ids.contains(tag.id)) { updatedFilter = currentFilter.copyWith( - tags: TagsQuery.fromIds( - currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList())); + tags: TagsQuery.fromIds( + currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(), + ), + ); } cubit.updateFilter(filter: updatedFilter); } on ErrorMessage catch (error) { showError(context, error); + } finally { + Navigator.pop(context); } } } diff --git a/lib/features/labels/view/pages/add_label_page.dart b/lib/features/labels/view/pages/add_label_page.dart index ed6b2dd..c660a44 100644 --- a/lib/features/labels/view/pages/add_label_page.dart +++ b/lib/features/labels/view/pages/add_label_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart'; import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:flutter_paperless_mobile/core/model/error_message.dart'; -import 'package:flutter_paperless_mobile/core/type/json.dart'; import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart'; import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart'; import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart'; @@ -40,14 +39,17 @@ class _AddLabelPageState extends State> { @override Widget build(BuildContext context) { return Scaffold( - resizeToAvoidBottomInset: false, + resizeToAvoidBottomInset: true, appBar: AppBar( title: Text(widget.addLabelStr), ), - floatingActionButton: FloatingActionButton.extended( - icon: const Icon(Icons.add), - label: Text(S.of(context).genericActionCreateLabel), - onPressed: _onSubmit, + floatingActionButton: Visibility( + visible: MediaQuery.of(context).viewInsets.bottom == 0, + child: FloatingActionButton.extended( + icon: const Icon(Icons.add), + label: Text(S.of(context).genericActionCreateLabel), + onPressed: _onSubmit, + ), ), body: FormBuilder( key: _formKey, @@ -99,7 +101,6 @@ class _AddLabelPageState extends State> { } void _onSubmit() async { - log("IsValid? ${_formKey.currentState?.isValid}"); if (_formKey.currentState?.saveAndValidate() ?? false) { try { final label = await widget.cubit.add(widget.fromJson(_formKey.currentState!.value)); diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index 0b6095b..83898bd 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart'; import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart'; @@ -9,7 +10,7 @@ import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; /// /// Form field allowing to select labels (i.e. correspondent, documentType) -/// [T] is the label (model) type, [R] is the return type. +/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...). /// class LabelFormField extends StatefulWidget { final Widget prefixIcon; @@ -23,6 +24,7 @@ class LabelFormField extends Statef final R Function() queryParameterNotAssignedBuilder; final R Function(int? id) queryParameterIdBuilder; final bool notAssignedSelectable; + final void Function(R?)? onChanged; const LabelFormField({ Key? key, @@ -37,6 +39,7 @@ class LabelFormField extends Statef required this.formBuilderState, required this.prefixIcon, this.notAssignedSelectable = true, + this.onChanged, }) : super(key: key); @override @@ -71,6 +74,14 @@ class _LabelFormFieldState @override Widget build(BuildContext context) { return FormBuilderTypeAhead( + noItemsFoundBuilder: (context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + S.of(context).labelFormFieldNoItemsFoundText, + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0), + ), + ), initialValue: widget.initialValue ?? widget.queryParameterIdBuilder(null), name: widget.name, itemBuilder: (context, suggestion) => ListTile( @@ -90,6 +101,7 @@ class _LabelFormFieldState }, onChanged: (value) { setState(() => _showClearSuffixIcon = value?.isSet ?? false); + widget.onChanged?.call(value as R); }, controller: _textEditingController, decoration: InputDecoration( diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart index 6b487ca..7b68c2a 100644 --- a/lib/features/scan/view/document_upload_page.dart +++ b/lib/features/scan/view/document_upload_page.dart @@ -64,108 +64,108 @@ class _DocumentUploadPageState extends State { child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0)) : null, ), - floatingActionButton: FloatingActionButton.extended( - onPressed: _onSubmit, - label: Text(S.of(context).genericActionUploadLabel), - icon: const Icon(Icons.upload), + floatingActionButton: Visibility( + visible: MediaQuery.of(context).viewInsets.bottom == 0, + child: FloatingActionButton.extended( + onPressed: _onSubmit, + label: Text(S.of(context).genericActionUploadLabel), + icon: const Icon(Icons.upload), + ), ), - body: SingleChildScrollView( - child: FormBuilder( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FormBuilderTextField( - autovalidateMode: AutovalidateMode.always, - name: DocumentModel.titleKey, - initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}", - validator: FormBuilderValidators.required(), - decoration: InputDecoration( - labelText: S.of(context).documentTitlePropertyLabel, - suffixIcon: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _formKey.currentState?.fields[DocumentModel.titleKey]?.didChange(""); - _formKey.currentState?.fields[fkFileName]?.didChange(".pdf"); - }, + body: FormBuilder( + key: _formKey, + child: ListView( + children: [ + FormBuilderTextField( + autovalidateMode: AutovalidateMode.always, + name: DocumentModel.titleKey, + initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}", + validator: FormBuilderValidators.required(), + decoration: InputDecoration( + labelText: S.of(context).documentTitlePropertyLabel, + suffixIcon: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _formKey.currentState?.fields[DocumentModel.titleKey]?.didChange(""); + _formKey.currentState?.fields[fkFileName]?.didChange(".pdf"); + }, + ), + errorText: _errors[DocumentModel.titleKey], + ), + onChanged: (value) { + final String? transformedValue = value?.replaceAll(RegExp(r"[\W_]"), "_"); + _formKey.currentState?.fields[fkFileName] + ?.didChange("${transformedValue ?? ''}.pdf"); + }, + ), + FormBuilderTextField( + autovalidateMode: AutovalidateMode.always, + readOnly: true, + enabled: false, + name: fkFileName, + decoration: InputDecoration( + labelText: S.of(context).documentUploadFileNameLabel, + ), + initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}.pdf", + ), + FormBuilderDateTimePicker( + autovalidateMode: AutovalidateMode.always, + format: DateFormat("dd. MMMM yyyy"), //TODO: INTL + inputType: InputType.date, + name: DocumentModel.createdKey, + initialValue: null, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.calendar_month_outlined), + labelText: S.of(context).documentCreatedPropertyLabel + " *", + ), + ), + BlocBuilder>( + bloc: getIt(), //TODO: Use provider + builder: (context, state) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialValue) => BlocProvider.value( + value: BlocProvider.of(context), + child: AddDocumentTypePage(initialName: initialValue), ), - errorText: _errors[DocumentModel.titleKey], - ), - onChanged: (value) { - final String? transformedValue = value?.replaceAll(RegExp(r"[\W_]"), "_"); - _formKey.currentState?.fields[fkFileName] - ?.didChange("${transformedValue ?? ''}.pdf"); - }, - ), - FormBuilderTextField( - autovalidateMode: AutovalidateMode.always, - readOnly: true, - enabled: false, - name: fkFileName, - decoration: InputDecoration( - labelText: S.of(context).documentUploadFileNameLabel, - ), - initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}.pdf", - ), - FormBuilderDateTimePicker( - autovalidateMode: AutovalidateMode.always, - format: DateFormat("dd. MMMM yyyy"), //TODO: INTL - inputType: InputType.date, - name: DocumentModel.createdKey, - initialValue: null, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.calendar_month_outlined), - labelText: S.of(context).documentCreatedPropertyLabel + " *", - ), - ), - BlocBuilder>( - bloc: getIt(), //TODO: Use provider - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => BlocProvider.value( - value: BlocProvider.of(context), - child: AddDocumentTypePage(initialName: initialValue), - ), - label: S.of(context).documentDocumentTypePropertyLabel + " *", - name: DocumentModel.documentTypeKey, - state: state, - queryParameterIdBuilder: DocumentTypeQuery.fromId, - queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, - prefixIcon: const Icon(Icons.description_outlined), - ); - }, - ), - BlocBuilder>( - bloc: getIt(), //TODO: Use provider - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => BlocProvider.value( - value: BlocProvider.of(context), - child: AddCorrespondentPage(initalValue: initialValue), - ), - label: S.of(context).documentCorrespondentPropertyLabel + " *", - name: DocumentModel.correspondentKey, - state: state, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, - prefixIcon: const Icon(Icons.person_outline), - ); - }, - ), - const TagFormField( - name: DocumentModel.tagsKey, - //Label: "Tags" + " *", - ), - Text( - "* " + S.of(context).uploadPageAutomaticallInferredFieldsHintText, - style: Theme.of(context).textTheme.caption, - ), - ].padded(), - ), + label: S.of(context).documentDocumentTypePropertyLabel + " *", + name: DocumentModel.documentTypeKey, + state: state, + queryParameterIdBuilder: DocumentTypeQuery.fromId, + queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, + prefixIcon: const Icon(Icons.description_outlined), + ); + }, + ), + BlocBuilder>( + bloc: getIt(), //TODO: Use provider + builder: (context, state) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialValue) => BlocProvider.value( + value: BlocProvider.of(context), + child: AddCorrespondentPage(initalValue: initialValue), + ), + label: S.of(context).documentCorrespondentPropertyLabel + " *", + name: DocumentModel.correspondentKey, + state: state, + queryParameterIdBuilder: CorrespondentQuery.fromId, + queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, + prefixIcon: const Icon(Icons.person_outline), + ); + }, + ), + const TagFormField( + name: DocumentModel.tagsKey, + //Label: "Tags" + " *", + ), + Text( + "* " + S.of(context).uploadPageAutomaticallInferredFieldsHintText, + style: Theme.of(context).textTheme.caption, + ), + ].padded(), ), ), ); diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 948c24c..3aed938 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -167,5 +167,6 @@ "errorMessageLoadSavedViewsError": "Gespeicherte Ansichten konnten nicht geladen werden.", "errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.", "errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht geklöscht werden, bitte versuche es erneut.", - "errorMessageRequestTimedOut": "Bei der Anfrage an den Server kam es zu einer Zeitüberschreitung." + "errorMessageRequestTimedOut": "Bei der Anfrage an den Server kam es zu einer Zeitüberschreitung.", + "labelFormFieldNoItemsFoundText": "Keine Treffer gefunden!" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index bd253dd..d5c2ed0 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -168,5 +168,6 @@ "errorMessageLoadSavedViewsError": "Could not load saved views.", "errorMessageCreateSavedViewError": "Could not create saved view, please try again.", "errorMessageDeleteSavedViewError": "Could not delete saved view, please try again", - "errorMessageRequestTimedOut": "The request to the server timed out." + "errorMessageRequestTimedOut": "The request to the server timed out.", + "labelFormFieldNoItemsFoundText": "No items found!" } \ No newline at end of file