import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; class DocumentEditPage extends StatefulWidget { final FieldSuggestions suggestions; const DocumentEditPage({ Key? key, required this.suggestions, }) : super(key: key); @override State createState() => _DocumentEditPageState(); } class _DocumentEditPageState extends State { static const fkTitle = "title"; static const fkCorrespondent = "correspondent"; static const fkTags = "tags"; static const fkDocumentType = "documentType"; static const fkCreatedDate = "createdAtDate"; static const fkStoragePath = 'storagePath'; final GlobalKey _formKey = GlobalKey(); bool _isSubmitLoading = false; late final FieldSuggestions _filteredSuggestions; @override void initState() { super.initState(); _filteredSuggestions = widget.suggestions .documentDifference(context.read().state.document); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: FloatingActionButton.extended( onPressed: () => _onSubmit(state.document), icon: const Icon(Icons.save), label: Text(S.of(context).saveChanges), ), appBar: AppBar( title: Text(S.of(context).editDocument), bottom: _isSubmitLoading ? const PreferredSize( preferredSize: Size.fromHeight(4), child: LinearProgressIndicator(), ) : null, ), extendBody: true, body: Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, top: 8, left: 8, right: 8, ), child: FormBuilder( key: _formKey, child: ListView( children: [ _buildTitleFormField(state.document.title).padded(), _buildCreatedAtFormField(state.document.created).padded(), _buildCorrespondentFormField( state.document.correspondent, state.correspondents, ).padded(), _buildDocumentTypeFormField( state.document.documentType, state.documentTypes, ).padded(), _buildStoragePathFormField( state.document.storagePath, state.storagePaths, ).padded(), TagFormField( initialValue: IdsTagsQuery.included(state.document.tags.toList()), notAssignedSelectable: false, anyAssignedSelectable: false, excludeAllowed: false, name: fkTags, selectableOptions: state.tags, suggestions: _filteredSuggestions.tags .toSet() .difference(state.document.tags.toSet()) .isNotEmpty ? _buildSuggestionsSkeleton( suggestions: _filteredSuggestions.tags, itemBuilder: (context, itemData) { final tag = state.tags[itemData]!; return ActionChip( label: Text( tag.name, style: TextStyle(color: tag.textColor), ), backgroundColor: tag.color, onPressed: () { final currentTags = _formKey.currentState ?.fields[fkTags]?.value as TagsQuery; if (currentTags is IdsTagsQuery) { _formKey.currentState?.fields[fkTags] ?.didChange((IdsTagsQuery.fromIds( {...currentTags.ids, itemData}))); } else { _formKey.currentState?.fields[fkTags] ?.didChange((IdsTagsQuery.fromIds( {itemData}))); } }, ); }, ) : null, ).padded(), const SizedBox( height: 64), // Prevent tags from being hidden by fab ], ), ), )); }, ); } Widget _buildStoragePathFormField( int? initialId, Map options, ) { return Column( children: [ LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( create: (context) => context.read>(), child: AddStoragePathPage(initalValue: initialValue), ), textFieldLabel: S.of(context).storagePath, labelOptions: options, initialValue: IdQueryParameter.fromId(initialId), name: fkStoragePath, prefixIcon: const Icon(Icons.folder_outlined), ), ], ); } Widget _buildCorrespondentFormField( int? initialId, Map options) { return Column( children: [ LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( create: (context) => context.read>(), child: AddCorrespondentPage(initialName: initialValue), ), textFieldLabel: S.of(context).correspondent, labelOptions: options, initialValue: IdQueryParameter.fromId(initialId), name: fkCorrespondent, prefixIcon: const Icon(Icons.person_outlined), ), if (_filteredSuggestions.hasSuggestedCorrespondents) _buildSuggestionsSkeleton( suggestions: _filteredSuggestions.correspondents, itemBuilder: (context, itemData) => ActionChip( label: Text(options[itemData]!.name), onPressed: () => _formKey.currentState?.fields[fkCorrespondent] ?.didChange((IdQueryParameter.fromId(itemData))), ), ), ], ); } Widget _buildDocumentTypeFormField( int? initialId, Map options, ) { return Column( children: [ LabelFormField( notAssignedSelectable: false, formBuilderState: _formKey.currentState, labelCreationWidgetBuilder: (currentInput) => RepositoryProvider( create: (context) => context.read>(), child: AddDocumentTypePage( initialName: currentInput, ), ), textFieldLabel: S.of(context).documentType, initialValue: IdQueryParameter.fromId(initialId), labelOptions: options, name: fkDocumentType, prefixIcon: const Icon(Icons.description_outlined), ), if (_filteredSuggestions.hasSuggestedDocumentTypes) _buildSuggestionsSkeleton( suggestions: _filteredSuggestions.documentTypes, itemBuilder: (context, itemData) => ActionChip( label: Text(options[itemData]!.name), onPressed: () => _formKey.currentState?.fields[fkDocumentType] ?.didChange(IdQueryParameter.fromId(itemData)), ), ), ], ); } Future _onSubmit(DocumentModel document) async { if (_formKey.currentState?.saveAndValidate() ?? false) { final values = _formKey.currentState!.value; var mergedDocument = document.copyWith( title: values[fkTitle], created: values[fkCreatedDate], documentType: () => (values[fkDocumentType] as IdQueryParameter).id, correspondent: () => (values[fkCorrespondent] as IdQueryParameter).id, storagePath: () => (values[fkStoragePath] as IdQueryParameter).id, tags: (values[fkTags] as IdsTagsQuery).includedIds, ); setState(() { _isSubmitLoading = true; }); try { await context.read().updateDocument(mergedDocument); showSnackBar(context, S.of(context).documentSuccessfullyUpdated); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { setState(() { _isSubmitLoading = false; }); Navigator.pop(context); } } } Widget _buildTitleFormField(String? initialTitle) { return FormBuilderTextField( name: fkTitle, validator: FormBuilderValidators.required(), decoration: InputDecoration( label: Text(S.of(context).title), ), initialValue: initialTitle, ); } Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FormBuilderDateTimePicker( inputType: InputType.date, name: fkCreatedDate, decoration: InputDecoration( prefixIcon: const Icon(Icons.calendar_month_outlined), label: Text(S.of(context).createdAt), ), initialValue: initialCreatedAtDate, format: DateFormat.yMMMMd(), initialEntryMode: DatePickerEntryMode.calendar, ), if (_filteredSuggestions.hasSuggestedDates) _buildSuggestionsSkeleton( suggestions: _filteredSuggestions.dates, itemBuilder: (context, itemData) => ActionChip( label: Text(DateFormat.yMMMd().format(itemData)), onPressed: () => _formKey.currentState?.fields[fkCreatedDate] ?.didChange(itemData), ), ), ], ); } /// /// Item builder is typically some sort of [Chip]. /// Widget _buildSuggestionsSkeleton({ required Iterable suggestions, required ItemBuilder itemBuilder, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( S.of(context).suggestions, style: Theme.of(context).textTheme.bodySmall, ), SizedBox( height: 48, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: suggestions.length, itemBuilder: (context, index) => ColoredChipWrapper( child: itemBuilder(context, suggestions.elementAt(index)), ), separatorBuilder: (BuildContext context, int index) => const SizedBox(width: 4.0), ), ), ], ).padded(); } }