Fixed FABs stacking on form fields, some other minor improvements

This commit is contained in:
Anton Stubenbord
2022-11-01 23:29:16 +01:00
parent f522991059
commit b4e5bf06b2
14 changed files with 313 additions and 279 deletions

View File

@@ -100,12 +100,19 @@ class DocumentsCubit extends Cubit<DocumentsState> {
/// Update filter state and automatically reload documents. Always resets page to 1. /// Update filter state and automatically reload documents. Always resets page to 1.
/// Use [DocumentsCubit.loadMore] to load more data. /// Use [DocumentsCubit.loadMore] to load more data.
Future<void> updateFilter({ Future<void> updateFilter({
DocumentFilter filter = DocumentFilter.initial, final DocumentFilter filter = DocumentFilter.initial,
}) async { }) async {
final result = await documentRepository.find(filter.copyWith(page: 1)); final result = await documentRepository.find(filter.copyWith(page: 1));
emit(DocumentsState(filter: filter, value: [result], isLoaded: true)); emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
} }
///
/// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
///
Future<void> updateCurrentFilter(final DocumentFilter Function(DocumentFilter) transformFn) {
return updateFilter(filter: transformFn(state.filter));
}
void toggleDocumentSelection(DocumentModel model) { void toggleDocumentSelection(DocumentModel model) {
if (state.selection.contains(model)) { if (state.selection.contains(model)) {
emit( emit(

View File

@@ -10,6 +10,7 @@ import 'package:flutter_paperless_mobile/features/documents/model/query_paramete
import 'package:flutter_paperless_mobile/util.dart'; import 'package:flutter_paperless_mobile/util.dart';
class DocumentFilter with EquatableMixin { class DocumentFilter with EquatableMixin {
static const _oneDay = Duration(days: 1);
static const DocumentFilter initial = DocumentFilter(); static const DocumentFilter initial = DocumentFilter();
static const DocumentFilter latestDocument = DocumentFilter( static const DocumentFilter latestDocument = DocumentFilter(
@@ -67,20 +68,21 @@ class DocumentFilter with EquatableMixin {
sb.write("&ordering=${sortOrder.queryString}${sortField.queryString}"); 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) { if (addedDateAfter != null) {
sb.write("&added__date__gt=${dateFormat.format(addedDateAfter!)}"); sb.write("&added__date__gt=${dateFormat.format(addedDateAfter!.subtract(_oneDay))}");
} }
if (addedDateBefore != null) { if (addedDateBefore != null) {
sb.write("&added__date__lt=${dateFormat.format(addedDateBefore!)}"); sb.write("&added__date__lt=${dateFormat.format(addedDateBefore!.add(_oneDay))}");
} }
if (createdDateAfter != null) { if (createdDateAfter != null) {
sb.write("&created__date__gt=${dateFormat.format(createdDateAfter!)}"); sb.write("&created__date__gt=${dateFormat.format(createdDateAfter!.subtract(_oneDay))}");
} }
if (createdDateBefore != null) { if (createdDateBefore != null) {
sb.write("&created__date__lt=${dateFormat.format(createdDateBefore!)}"); sb.write("&created__date__lt=${dateFormat.format(createdDateBefore!.add(_oneDay))}");
} }
return sb.toString(); return sb.toString();

View File

@@ -37,7 +37,7 @@ abstract class IdQueryParameter extends Equatable {
return "&${queryParameterKey}__isnull=$_assignmentStatus"; return "&${queryParameterKey}__isnull=$_assignmentStatus";
} }
if (isSet) { if (isSet) {
return "${queryParameterKey}__id=$id"; return "&${queryParameterKey}__id=$id";
} }
return ""; return "";
} }

View File

@@ -122,9 +122,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
controller: _panelController, controller: _panelController,
defaultPanelState: PanelState.CLOSED, defaultPanelState: PanelState.CLOSED,
minHeight: 48, minHeight: 48,
maxHeight: MediaQuery.of(context).size.height - maxHeight: MediaQuery.of(context).size.height - kBottomNavigationBarHeight,
kBottomNavigationBarHeight -
2 * kToolbarHeight,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),

View File

@@ -2,27 +2,26 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.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_cubit.dart';
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.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/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/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/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/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/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/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/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/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:flutter_paperless_mobile/generated/l10n.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
@@ -62,7 +61,14 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
]; ];
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
bool _isQueryLoading = false;
late final DocumentsCubit _documentsCubit;
@override
void initState() {
super.initState();
_documentsCubit = BlocProvider.of<DocumentsCubit>(context);
}
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) { DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
if (start == null && end == null) { if (start == null && end == null) {
@@ -78,18 +84,21 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<DocumentsCubit, DocumentsState>( return ClipRRect(
listener: (context, state) { borderRadius: const BorderRadius.only(
// Set initial values, otherwise they would not automatically update. topLeft: Radius.circular(16),
_patchFromFilter(state.filter); topRight: Radius.circular(16),
}, ),
builder: (context, state) { child: BlocConsumer<DocumentsCubit, DocumentsState>(
return FormBuilder( listener: (context, state) {
key: _formKey, // Set initial values, otherwise they would not automatically update.
child: MediaQuery.removePadding( _patchFromFilter(state.filter);
context: context, },
removeTop: true, builder: (context, state) {
child: Column( return FormBuilder(
key: _formKey,
child: ListView(
controller: widget.scrollController,
children: [ children: [
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
@@ -121,40 +130,37 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
], ],
).padded(), ).padded(),
Expanded( const SizedBox(
child: ListView( height: 16.0,
controller: widget.scrollController, ),
children: [ Align(
const SizedBox( alignment: Alignment.centerLeft,
height: 16.0, child: Text(S.of(context).documentsFilterPageSearchLabel),
), ).padded(const EdgeInsets.only(left: 8.0)),
Align( _buildQueryFormField(state),
alignment: Alignment.centerLeft, _buildSortByChipsList(context, state),
child: Text(S.of(context).documentsFilterPageSearchLabel), Align(
).padded(), alignment: Alignment.centerLeft,
_buildQueryFormField(state), child: Text(S.of(context).documentsFilterPageAdvancedLabel),
_buildSortByChipsList(context, state), ).padded(const EdgeInsets.only(left: 8.0, top: 8.0)),
Align( _buildCreatedDateRangePickerFormField(state).padded(),
alignment: Alignment.centerLeft, _buildAddedDateRangePickerFormField(state).padded(),
child: Text(S.of(context).documentsFilterPageAdvancedLabel), _buildCorrespondentFormField(state).padded(),
).padded(), _buildDocumentTypeFormField(state).padded(),
_buildCreatedDateRangePickerFormField(state).padded(), _buildStoragePathFormField(state).padded(),
_buildAddedDateRangePickerFormField(state).padded(), TagFormField(
_buildCorrespondentFormField(state).padded(), name: DocumentModel.tagsKey,
_buildDocumentTypeFormField(state).padded(), initialValue: state.filter.tags,
_buildStoragePathFormField(state).padded(), ).padded(),
TagFormField( // Required in order for the storage path field to be visible when typing
name: DocumentModel.tagsKey, const SizedBox(
initialValue: state.filter.tags, height: 200,
).padded(),
],
),
), ),
], ],
), ),
), );
); },
}, ),
); );
} }
@@ -184,6 +190,23 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildCorrespondentFormField(DocumentsState docState) {
return BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
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) { Widget _buildStoragePathFormField(DocumentsState docState) {
return BlocBuilder<StoragePathCubit, Map<int, StoragePath>>( return BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
builder: (context, state) { builder: (context, state) {
@@ -308,23 +331,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildCorrespondentFormField(DocumentsState docState) {
return BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
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) { Widget _buildCreatedDateRangePickerFormField(DocumentsState state) {
return Column( return Column(
children: [ children: [
@@ -333,16 +339,21 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
state.filter.createdDateAfter, state.filter.createdDateAfter,
state.filter.createdDateBefore, state.filter.createdDateBefore,
), ),
pickerBuilder: (context, child) { // Workaround for theme data not being correctly passed to daterangepicker, see
return Theme( // https://github.com/flutter/flutter/issues/87580
data: ThemeData.light().copyWith( pickerBuilder: (context, Widget? child) => Theme(
primaryColor: Theme.of(context).primaryColor, data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme, dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
buttonTheme: Theme.of(context).buttonTheme, appBarTheme: Theme.of(context).appBarTheme.copyWith(
), iconTheme: IconThemeData(color: Theme.of(context).primaryColor),
child: child!, ),
); 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()), format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel, fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel,
fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel, fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel,
@@ -371,16 +382,21 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
state.filter.addedDateAfter, state.filter.addedDateAfter,
state.filter.addedDateBefore, state.filter.addedDateBefore,
), ),
pickerBuilder: (context, child) { // Workaround for theme data not being correctly passed to daterangepicker, see
return Theme( // https://github.com/flutter/flutter/issues/87580
data: ThemeData.light().copyWith( pickerBuilder: (context, Widget? child) => Theme(
primaryColor: Theme.of(context).primaryColor, data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme, dialogBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
buttonTheme: Theme.of(context).buttonTheme, appBarTheme: Theme.of(context).appBarTheme.copyWith(
), iconTheme: IconThemeData(color: Theme.of(context).primaryColor),
child: child!, ),
); 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()), format: DateFormat.yMMMd(Localizations.localeOf(context).toString()),
fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel, fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel,
fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel, fieldEndLabelText: S.of(context).documentsFilterPageDateRangeFieldEndLabel,
@@ -413,30 +429,27 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
} }
Widget _buildSortByChipsList(BuildContext context, DocumentsState state) { Widget _buildSortByChipsList(BuildContext context, DocumentsState state) {
return Padding( return Column(
padding: const EdgeInsets.all(8.0), mainAxisAlignment: MainAxisAlignment.end,
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end, children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ S.of(context).documentsPageOrderByLabel,
Text( ),
S.of(context).documentsPageOrderByLabel, SizedBox(
), height: kToolbarHeight,
SizedBox( child: ListView.separated(
height: kToolbarHeight, itemCount: _sortFields.length,
child: ListView.separated( scrollDirection: Axis.horizontal,
itemCount: _sortFields.length, separatorBuilder: (context, index) => const SizedBox(
scrollDirection: Axis.horizontal, width: 8.0,
separatorBuilder: (context, index) => const SizedBox(
width: 8.0,
),
itemBuilder: (context, index) =>
_buildActionChip(_sortFields[index], state.filter.sortField, context),
), ),
itemBuilder: (context, index) =>
_buildActionChip(_sortFields[index], state.filter.sortField, context),
), ),
], ),
), ],
); ).padded();
} }
Widget _buildActionChip( Widget _buildActionChip(
@@ -481,9 +494,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
} }
void _onApplyFilter() { void _onApplyFilter() {
setState(() => _isQueryLoading = true); if (_formKey.currentState?.saveAndValidate() ?? false) {
_formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) {
final v = _formKey.currentState!.value; final v = _formKey.currentState!.value;
final docCubit = BlocProvider.of<DocumentsCubit>(context); final docCubit = BlocProvider.of<DocumentsCubit>(context);
DocumentFilter newFilter = docCubit.state.filter.copyWith( DocumentFilter newFilter = docCubit.state.filter.copyWith(
@@ -503,7 +514,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
BlocProvider.of<SavedViewCubit>(context).resetSelection(); BlocProvider.of<SavedViewCubit>(context).resetSelection();
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
widget.panelController.close(); widget.panelController.close();
setState(() => _isQueryLoading = false);
}); });
} }
} }

View File

@@ -43,15 +43,14 @@ class CorrespondentWidget extends StatelessWidget {
} }
void _addCorrespondentToFilter(BuildContext context) { void _addCorrespondentToFilter(BuildContext context) {
final cubit = getIt<DocumentsCubit>(); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.correspondent.id == correspondentId) { if (cubit.state.filter.correspondent.id == correspondentId) {
cubit.updateFilter( cubit.updateCurrentFilter(
filter: cubit.state.filter.copyWith(correspondent: const CorrespondentQuery.unset())); (filter) => filter.copyWith(correspondent: const CorrespondentQuery.unset()),
);
} else { } else {
cubit.updateFilter( cubit.updateCurrentFilter(
filter: cubit.state.filter.copyWith( (filter) => filter.copyWith(correspondent: CorrespondentQuery.fromId(correspondentId)),
correspondent: CorrespondentQuery.fromId(correspondentId),
),
); );
} }
afterSelected?.call(); afterSelected?.call();

View File

@@ -1,52 +1,53 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package: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/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/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'; import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
class DocumentTypeWidget extends StatelessWidget { class DocumentTypeWidget extends StatelessWidget {
final int? documentTypeId; final int? documentTypeId;
final void Function()? afterSelected; final void Function()? afterSelected;
final bool isSelectable;
const DocumentTypeWidget({ const DocumentTypeWidget({
Key? key, Key? key,
required this.documentTypeId, required this.documentTypeId,
this.afterSelected, this.afterSelected,
this.isSelectable = true,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return AbsorbPointer(
onTap: _addDocumentTypeToFilter, absorbing: !isSelectable,
child: BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>( child: GestureDetector(
builder: (context, state) { onTap: () => _addDocumentTypeToFilter(context),
return Text( child: BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
state[documentTypeId]?.toString() ?? "-", builder: (context, state) {
style: Theme.of(context) return Text(
.textTheme state[documentTypeId]?.toString() ?? "-",
.bodyText2! style: Theme.of(context)
.copyWith(color: Theme.of(context).colorScheme.primary), .textTheme
); .bodyText2!
}, .copyWith(color: Theme.of(context).colorScheme.tertiary),
);
},
),
), ),
); );
} }
void _addDocumentTypeToFilter() { void _addDocumentTypeToFilter(BuildContext context) {
final cubit = getIt<DocumentsCubit>(); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.documentType.id == documentTypeId) { if (cubit.state.filter.documentType.id == documentTypeId) {
cubit.updateFilter( cubit.updateCurrentFilter(
filter: cubit.state.filter.copyWith(documentType: const DocumentTypeQuery.unset())); (filter) => filter.copyWith(documentType: const DocumentTypeQuery.unset()),
);
} else { } else {
cubit.updateFilter( cubit.updateCurrentFilter(
filter: cubit.state.filter.copyWith( (filter) => filter.copyWith(documentType: DocumentTypeQuery.fromId(documentTypeId)),
documentType: DocumentTypeQuery.fromId(documentTypeId),
),
); );
} }
if (afterSelected != null) { afterSelected?.call();
afterSelected?.call();
}
} }
} }

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_paperless_mobile/di_initializer.dart';
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.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/documents/model/query_parameters/storage_path_query.dart';
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.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) { void _addStoragePathToFilter(BuildContext context) {
final cubit = getIt<DocumentsCubit>(); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.correspondent.id == pathId) { if (cubit.state.filter.correspondent.id == pathId) {
cubit.updateFilter( cubit.updateCurrentFilter(
filter: cubit.state.filter.copyWith(storagePath: const StoragePathQuery.unset())); (filter) => filter.copyWith(storagePath: const StoragePathQuery.unset()),
);
} else { } else {
cubit.updateFilter( cubit.updateCurrentFilter(
filter: cubit.state.filter.copyWith( (filter) => filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)),
storagePath: StoragePathQuery.fromId(pathId),
),
); );
} }
afterSelected?.call(); afterSelected?.call();

View File

@@ -50,12 +50,16 @@ class EditTagPage extends StatelessWidget {
late DocumentFilter updatedFilter = currentFilter; late DocumentFilter updatedFilter = currentFilter;
if (currentFilter.tags.ids.contains(tag.id)) { if (currentFilter.tags.ids.contains(tag.id)) {
updatedFilter = currentFilter.copyWith( updatedFilter = currentFilter.copyWith(
tags: TagsQuery.fromIds( tags: TagsQuery.fromIds(
currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList())); currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(),
),
);
} }
cubit.updateFilter(filter: updatedFilter); cubit.updateFilter(filter: updatedFilter);
} on ErrorMessage catch (error) { } on ErrorMessage catch (error) {
showError(context, error); showError(context, error);
} finally {
Navigator.pop(context);
} }
} }
} }

View File

@@ -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/bloc/label_cubit.dart';
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.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/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/extensions/flutter_extensions.dart';
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.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'; import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
@@ -40,14 +39,17 @@ class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
title: Text(widget.addLabelStr), title: Text(widget.addLabelStr),
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: Visibility(
icon: const Icon(Icons.add), visible: MediaQuery.of(context).viewInsets.bottom == 0,
label: Text(S.of(context).genericActionCreateLabel), child: FloatingActionButton.extended(
onPressed: _onSubmit, icon: const Icon(Icons.add),
label: Text(S.of(context).genericActionCreateLabel),
onPressed: _onSubmit,
),
), ),
body: FormBuilder( body: FormBuilder(
key: _formKey, key: _formKey,
@@ -99,7 +101,6 @@ class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
} }
void _onSubmit() async { void _onSubmit() async {
log("IsValid? ${_formKey.currentState?.isValid}");
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
try { try {
final label = await widget.cubit.add(widget.fromJson(_formKey.currentState!.value)); final label = await widget.cubit.add(widget.fromJson(_formKey.currentState!.value));

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/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/correspondent/model/correspondent.model.dart';
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.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) /// 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<T extends Label, R extends IdQueryParameter> extends StatefulWidget { class LabelFormField<T extends Label, R extends IdQueryParameter> extends StatefulWidget {
final Widget prefixIcon; final Widget prefixIcon;
@@ -23,6 +24,7 @@ class LabelFormField<T extends Label, R extends IdQueryParameter> extends Statef
final R Function() queryParameterNotAssignedBuilder; final R Function() queryParameterNotAssignedBuilder;
final R Function(int? id) queryParameterIdBuilder; final R Function(int? id) queryParameterIdBuilder;
final bool notAssignedSelectable; final bool notAssignedSelectable;
final void Function(R?)? onChanged;
const LabelFormField({ const LabelFormField({
Key? key, Key? key,
@@ -37,6 +39,7 @@ class LabelFormField<T extends Label, R extends IdQueryParameter> extends Statef
required this.formBuilderState, required this.formBuilderState,
required this.prefixIcon, required this.prefixIcon,
this.notAssignedSelectable = true, this.notAssignedSelectable = true,
this.onChanged,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -71,6 +74,14 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FormBuilderTypeAhead<IdQueryParameter>( return FormBuilderTypeAhead<IdQueryParameter>(
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), initialValue: widget.initialValue ?? widget.queryParameterIdBuilder(null),
name: widget.name, name: widget.name,
itemBuilder: (context, suggestion) => ListTile( itemBuilder: (context, suggestion) => ListTile(
@@ -90,6 +101,7 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
}, },
onChanged: (value) { onChanged: (value) {
setState(() => _showClearSuffixIcon = value?.isSet ?? false); setState(() => _showClearSuffixIcon = value?.isSet ?? false);
widget.onChanged?.call(value as R);
}, },
controller: _textEditingController, controller: _textEditingController,
decoration: InputDecoration( decoration: InputDecoration(

View File

@@ -64,108 +64,108 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0)) child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0))
: null, : null,
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: Visibility(
onPressed: _onSubmit, visible: MediaQuery.of(context).viewInsets.bottom == 0,
label: Text(S.of(context).genericActionUploadLabel), child: FloatingActionButton.extended(
icon: const Icon(Icons.upload), onPressed: _onSubmit,
label: Text(S.of(context).genericActionUploadLabel),
icon: const Icon(Icons.upload),
),
), ),
body: SingleChildScrollView( body: FormBuilder(
child: FormBuilder( key: _formKey,
key: _formKey, child: ListView(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, FormBuilderTextField(
children: [ autovalidateMode: AutovalidateMode.always,
FormBuilderTextField( name: DocumentModel.titleKey,
autovalidateMode: AutovalidateMode.always, initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}",
name: DocumentModel.titleKey, validator: FormBuilderValidators.required(),
initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}", decoration: InputDecoration(
validator: FormBuilderValidators.required(), labelText: S.of(context).documentTitlePropertyLabel,
decoration: InputDecoration( suffixIcon: IconButton(
labelText: S.of(context).documentTitlePropertyLabel, icon: const Icon(Icons.close),
suffixIcon: IconButton( onPressed: () {
icon: const Icon(Icons.close), _formKey.currentState?.fields[DocumentModel.titleKey]?.didChange("");
onPressed: () { _formKey.currentState?.fields[fkFileName]?.didChange(".pdf");
_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<DocumentTypeCubit, Map<int, DocumentType>>(
bloc: getIt<DocumentTypeCubit>(), //TODO: Use provider
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
value: BlocProvider.of<DocumentTypeCubit>(context),
child: AddDocumentTypePage(initialName: initialValue),
), ),
errorText: _errors[DocumentModel.titleKey], label: S.of(context).documentDocumentTypePropertyLabel + " *",
), name: DocumentModel.documentTypeKey,
onChanged: (value) { state: state,
final String? transformedValue = value?.replaceAll(RegExp(r"[\W_]"), "_"); queryParameterIdBuilder: DocumentTypeQuery.fromId,
_formKey.currentState?.fields[fkFileName] queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
?.didChange("${transformedValue ?? ''}.pdf"); prefixIcon: const Icon(Icons.description_outlined),
}, );
), },
FormBuilderTextField( ),
autovalidateMode: AutovalidateMode.always, BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
readOnly: true, bloc: getIt<CorrespondentCubit>(), //TODO: Use provider
enabled: false, builder: (context, state) {
name: fkFileName, return LabelFormField<Correspondent, CorrespondentQuery>(
decoration: InputDecoration( notAssignedSelectable: false,
labelText: S.of(context).documentUploadFileNameLabel, formBuilderState: _formKey.currentState,
), labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}.pdf", value: BlocProvider.of<CorrespondentCubit>(context),
), child: AddCorrespondentPage(initalValue: initialValue),
FormBuilderDateTimePicker( ),
autovalidateMode: AutovalidateMode.always, label: S.of(context).documentCorrespondentPropertyLabel + " *",
format: DateFormat("dd. MMMM yyyy"), //TODO: INTL name: DocumentModel.correspondentKey,
inputType: InputType.date, state: state,
name: DocumentModel.createdKey, queryParameterIdBuilder: CorrespondentQuery.fromId,
initialValue: null, queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
decoration: InputDecoration( prefixIcon: const Icon(Icons.person_outline),
prefixIcon: const Icon(Icons.calendar_month_outlined), );
labelText: S.of(context).documentCreatedPropertyLabel + " *", },
), ),
), const TagFormField(
BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>( name: DocumentModel.tagsKey,
bloc: getIt<DocumentTypeCubit>(), //TODO: Use provider //Label: "Tags" + " *",
builder: (context, state) { ),
return LabelFormField<DocumentType, DocumentTypeQuery>( Text(
notAssignedSelectable: false, "* " + S.of(context).uploadPageAutomaticallInferredFieldsHintText,
formBuilderState: _formKey.currentState, style: Theme.of(context).textTheme.caption,
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value( ),
value: BlocProvider.of<DocumentTypeCubit>(context), ].padded(),
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<CorrespondentCubit, Map<int, Correspondent>>(
bloc: getIt<CorrespondentCubit>(), //TODO: Use provider
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => BlocProvider.value(
value: BlocProvider.of<CorrespondentCubit>(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(),
),
), ),
), ),
); );

View File

@@ -167,5 +167,6 @@
"errorMessageLoadSavedViewsError": "Gespeicherte Ansichten konnten nicht geladen werden.", "errorMessageLoadSavedViewsError": "Gespeicherte Ansichten konnten nicht geladen werden.",
"errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.", "errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.",
"errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht geklöscht 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!"
} }

View File

@@ -168,5 +168,6 @@
"errorMessageLoadSavedViewsError": "Could not load saved views.", "errorMessageLoadSavedViewsError": "Could not load saved views.",
"errorMessageCreateSavedViewError": "Could not create saved view, please try again.", "errorMessageCreateSavedViewError": "Could not create saved view, please try again.",
"errorMessageDeleteSavedViewError": "Could not delete 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!"
} }