diff --git a/integration_test/src/framework.dart b/integration_test/src/framework.dart index 06717a8..1ba69d6 100644 --- a/integration_test/src/framework.dart +++ b/integration_test/src/framework.dart @@ -1,6 +1,4 @@ -import 'dart:ui'; - -import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:paperless_mobile/di_initializer.dart'; diff --git a/lib/core/bloc/bloc_changes_observer.dart b/lib/core/bloc/bloc_changes_observer.dart index 5e9e925..f06f670 100644 --- a/lib/core/bloc/bloc_changes_observer.dart +++ b/lib/core/bloc/bloc_changes_observer.dart @@ -1,7 +1,4 @@ -import 'dart:developer'; - import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; class BlocChangesObserver extends BlocObserver { @override diff --git a/lib/core/repository/impl/correspondent_repository_impl.dart b/lib/core/repository/impl/correspondent_repository_impl.dart new file mode 100644 index 0000000..cf2ae74 --- /dev/null +++ b/lib/core/repository/impl/correspondent_repository_impl.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:rxdart/rxdart.dart' show BehaviorSubject; + +class CorrespondentRepositoryImpl implements LabelRepository { + final PaperlessLabelsApi _api; + + final _subject = BehaviorSubject>.seeded(const {}); + + CorrespondentRepositoryImpl(this._api); + @override + Stream> get labels => + _subject.stream.asBroadcastStream(); + + @override + Future create(Correspondent correspondent) async { + final created = await _api.saveCorrespondent(correspondent); + final updatedState = {..._subject.value} + ..putIfAbsent(created.id!, () => created); + _subject.add(updatedState); + return created; + } + + @override + Future delete(Correspondent correspondent) async { + await _api.deleteCorrespondent(correspondent); + final updatedState = {..._subject.value} + ..removeWhere((k, v) => k == correspondent.id); + _subject.add(updatedState); + } + + @override + Future find(int id) async { + final correspondent = await _api.getCorrespondent(id); + if (correspondent != null) { + final updatedState = {..._subject.value}..[id] = correspondent; + _subject.add(updatedState); + return correspondent; + } + return null; + } + + @override + Future> findAll([Iterable? ids]) async { + final correspondents = await _api.getCorrespondents(ids); + final updatedState = {..._subject.value} + ..addEntries(correspondents.map((e) => MapEntry(e.id!, e))); + _subject.add(updatedState); + return correspondents; + } + + @override + Future update(Correspondent correspondent) async { + final updated = await _api.updateCorrespondent(correspondent); + final updatedState = {..._subject.value} + ..update(updated.id!, (_) => updated); + _subject.add(updatedState); + return updated; + } + + @override + void clear() { + _subject.add(const {}); + } +} diff --git a/lib/core/repository/impl/document_type_repository_impl.dart b/lib/core/repository/impl/document_type_repository_impl.dart new file mode 100644 index 0000000..60ee381 --- /dev/null +++ b/lib/core/repository/impl/document_type_repository_impl.dart @@ -0,0 +1,66 @@ +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:rxdart/rxdart.dart' show BehaviorSubject; + +class DocumentTypeRepositoryImpl implements LabelRepository { + final PaperlessLabelsApi _api; + + final _subject = BehaviorSubject>.seeded(const {}); + + DocumentTypeRepositoryImpl(this._api); + + @override + Stream> get labels => + _subject.stream.asBroadcastStream(); + + @override + Future create(DocumentType documentType) async { + final created = await _api.saveDocumentType(documentType); + final updatedState = {..._subject.value} + ..putIfAbsent(created.id!, () => created); + _subject.add(updatedState); + return created; + } + + @override + Future delete(DocumentType documentType) async { + await _api.deleteDocumentType(documentType); + final updatedState = {..._subject.value} + ..removeWhere((k, v) => k == documentType.id); + _subject.add(updatedState); + } + + @override + Future find(int id) async { + final documentType = await _api.getDocumentType(id); + if (documentType != null) { + final updatedState = {..._subject.value}..[id] = documentType; + _subject.add(updatedState); + return documentType; + } + return null; + } + + @override + Future> findAll([Iterable? ids]) async { + final documentTypes = await _api.getDocumentTypes(ids); + final updatedState = {..._subject.value} + ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); + _subject.add(updatedState); + return documentTypes; + } + + @override + Future update(DocumentType documentType) async { + final updated = await _api.updateDocumentType(documentType); + final updatedState = {..._subject.value} + ..update(updated.id!, (_) => updated); + _subject.add(updatedState); + return updated; + } + + @override + void clear() { + _subject.add(const {}); + } +} diff --git a/lib/core/repository/impl/saved_view_repository_impl.dart b/lib/core/repository/impl/saved_view_repository_impl.dart new file mode 100644 index 0000000..6997548 --- /dev/null +++ b/lib/core/repository/impl/saved_view_repository_impl.dart @@ -0,0 +1,57 @@ +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_api/src/models/saved_view_model.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:rxdart/rxdart.dart'; + +class SavedViewRepositoryImpl implements SavedViewRepository { + final PaperlessSavedViewsApi _api; + + SavedViewRepositoryImpl(this._api); + + final BehaviorSubject> _subject = + BehaviorSubject.seeded({}); + + @override + Stream> get savedViews => + _subject.stream.asBroadcastStream(); + + @override + void clear() {} + + @override + Future create(SavedView view) async { + final created = await _api.save(view); + final updatedState = {..._subject.value} + ..putIfAbsent(created.id!, () => created); + _subject.add(updatedState); + return created; + } + + @override + Future delete(SavedView view) async { + await _api.delete(view); + final updatedState = {..._subject.value}..remove(view.id); + _subject.add(updatedState); + return view.id!; + } + + @override + Future find(int id) async { + final found = await _api.find(id); + final updatedState = {..._subject.value} + ..update(id, (_) => found, ifAbsent: () => found); + _subject.add(updatedState); + return found; + } + + @override + Future> findAll([Iterable? ids]) async { + final found = await _api.findAll(ids); + final updatedState = { + ..._subject.value, + ...{for (final view in found) view.id!: view}, + }; + _subject.add(updatedState); + return found; + } +} diff --git a/lib/core/repository/impl/storage_path_repository_impl.dart b/lib/core/repository/impl/storage_path_repository_impl.dart new file mode 100644 index 0000000..2e3d608 --- /dev/null +++ b/lib/core/repository/impl/storage_path_repository_impl.dart @@ -0,0 +1,66 @@ +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:rxdart/rxdart.dart' show BehaviorSubject; + +class StoragePathRepositoryImpl implements LabelRepository { + final PaperlessLabelsApi _api; + + final _subject = BehaviorSubject>.seeded(const {}); + + StoragePathRepositoryImpl(this._api); + + @override + Stream> get labels => + _subject.stream.asBroadcastStream(); + + @override + Future create(StoragePath storagePath) async { + final created = await _api.saveStoragePath(storagePath); + final updatedState = {..._subject.value} + ..putIfAbsent(created.id!, () => created); + _subject.add(updatedState); + return created; + } + + @override + Future delete(StoragePath storagePath) async { + await _api.deleteStoragePath(storagePath); + final updatedState = {..._subject.value} + ..removeWhere((k, v) => k == storagePath.id); + _subject.add(updatedState); + } + + @override + Future find(int id) async { + final storagePath = await _api.getStoragePath(id); + if (storagePath != null) { + final updatedState = {..._subject.value}..[id] = storagePath; + _subject.add(updatedState); + return storagePath; + } + return null; + } + + @override + Future> findAll([Iterable? ids]) async { + final storagePaths = await _api.getStoragePaths(ids); + final updatedState = {..._subject.value} + ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); + _subject.add(updatedState); + return storagePaths; + } + + @override + Future update(StoragePath storagePath) async { + final updated = await _api.updateStoragePath(storagePath); + final updatedState = {..._subject.value} + ..update(updated.id!, (_) => updated); + _subject.add(updatedState); + return updated; + } + + @override + void clear() { + _subject.add(const {}); + } +} diff --git a/lib/core/repository/impl/tag_repository_impl.dart b/lib/core/repository/impl/tag_repository_impl.dart new file mode 100644 index 0000000..c479cea --- /dev/null +++ b/lib/core/repository/impl/tag_repository_impl.dart @@ -0,0 +1,65 @@ +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:rxdart/rxdart.dart' show BehaviorSubject; + +class TagRepositoryImpl implements LabelRepository { + final PaperlessLabelsApi _api; + + final _subject = BehaviorSubject>.seeded(const {}); + + TagRepositoryImpl(this._api); + + @override + Stream> get labels => _subject.stream.asBroadcastStream(); + + @override + Future create(Tag tag) async { + final created = await _api.saveTag(tag); + final updatedState = {..._subject.value} + ..putIfAbsent(created.id!, () => created); + _subject.add(updatedState); + return created; + } + + @override + Future delete(Tag tag) async { + await _api.deleteTag(tag); + final updatedState = {..._subject.value} + ..removeWhere((k, v) => k == tag.id); + _subject.add(updatedState); + } + + @override + Future find(int id) async { + final tag = await _api.getTag(id); + if (tag != null) { + final updatedState = {..._subject.value}..[id] = tag; + _subject.add(updatedState); + return tag; + } + return null; + } + + @override + Future> findAll([Iterable? ids]) async { + final tags = await _api.getTags(ids); + final updatedState = {..._subject.value} + ..addEntries(tags.map((e) => MapEntry(e.id!, e))); + _subject.add(updatedState); + return tags; + } + + @override + Future update(Tag tag) async { + final updated = await _api.updateTag(tag); + final updatedState = {..._subject.value} + ..update(updated.id!, (_) => updated); + _subject.add(updatedState); + return updated; + } + + @override + void clear() { + _subject.add(const {}); + } +} diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart new file mode 100644 index 0000000..848681c --- /dev/null +++ b/lib/core/repository/label_repository.dart @@ -0,0 +1,13 @@ +import 'package:paperless_api/paperless_api.dart'; + +abstract class LabelRepository { + Stream> get labels; + + Future create(T label); + Future find(int id); + Future> findAll([Iterable? ids]); + Future update(T label); + Future delete(T label); + + void clear(); +} diff --git a/lib/core/repository/provider/label_repositories_provider.dart b/lib/core/repository/provider/label_repositories_provider.dart new file mode 100644 index 0000000..21a82d7 --- /dev/null +++ b/lib/core/repository/provider/label_repositories_provider.dart @@ -0,0 +1,31 @@ +import 'package:flutter/src/widgets/container.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; + +class LabelRepositoriesProvider extends StatelessWidget { + final Widget child; + const LabelRepositoriesProvider({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return MultiRepositoryProvider( + providers: [ + RepositoryProvider.value( + value: RepositoryProvider.of>(context), + ), + RepositoryProvider.value( + value: RepositoryProvider.of>(context), + ), + RepositoryProvider.value( + value: RepositoryProvider.of>(context), + ), + RepositoryProvider.value( + value: RepositoryProvider.of>(context), + ), + ], + child: child, + ); + } +} diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart new file mode 100644 index 0000000..cb57caf --- /dev/null +++ b/lib/core/repository/saved_view_repository.dart @@ -0,0 +1,12 @@ +import 'package:paperless_api/paperless_api.dart'; + +abstract class SavedViewRepository { + Stream> get savedViews; + + Future create(SavedView view); + Future find(int id); + Future> findAll([Iterable? ids]); + Future delete(SavedView view); + + void clear(); +} diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 546e7ed..1e74c3b 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/date_symbol_data_local.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/widgets/highlighted_text.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -17,7 +18,6 @@ import 'package:paperless_mobile/features/documents/view/pages/document_edit_pag import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; @@ -213,7 +213,29 @@ class _DocumentDetailsPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( + builder: (_) => MultiRepositoryProvider( + providers: [ + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + ), + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + ), + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + ), + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + ), + ], child: DocumentEditPage( document: cubit.state.document!, onEdit: (updatedDocument) { diff --git a/lib/features/document_upload/cubit/document_upload_cubit.dart b/lib/features/document_upload/cubit/document_upload_cubit.dart new file mode 100644 index 0000000..e870a21 --- /dev/null +++ b/lib/features/document_upload/cubit/document_upload_cubit.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/store/local_vault.dart'; + +part 'document_upload_state.dart'; + +class DocumentUploadCubit extends Cubit { + final LocalVault _localVault; + final PaperlessDocumentsApi _documentApi; + + final LabelRepository _tagRepository; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; + + final List _subs = const []; + + DocumentUploadCubit({ + required LocalVault localVault, + required PaperlessDocumentsApi documentApi, + required LabelRepository tagRepository, + required LabelRepository correspondentRepository, + required LabelRepository documentTypeRepository, + }) : _documentApi = documentApi, + _tagRepository = tagRepository, + _correspondentRepository = correspondentRepository, + _documentTypeRepository = documentTypeRepository, + _localVault = localVault, + super( + const DocumentUploadState( + tags: {}, + correspondents: {}, + documentTypes: {}, + ), + ) { + _subs.add(_tagRepository.labels.listen( + (tags) => emit(state.copyWith(tags: tags)), + )); + _subs.add(_correspondentRepository.labels.listen( + (correspondents) => emit(state.copyWith(correspondents: correspondents)), + )); + _subs.add(_documentTypeRepository.labels.listen( + (documentTypes) => emit(state.copyWith(documentTypes: documentTypes)), + )); + } + + Future upload( + Uint8List bytes, { + required String filename, + required String title, + required void Function(DocumentModel document)? onConsumptionFinished, + int? documentType, + int? correspondent, + Iterable tags = const [], + DateTime? createdAt, + }) async { + final auth = await _localVault.loadAuthenticationInformation(); + if (auth == null || !auth.isValid) { + throw const PaperlessServerException(ErrorCode.notAuthenticated); + } + await _documentApi.create( + bytes, + filename: filename, + title: title, + correspondent: correspondent, + documentType: documentType, + tags: tags, + createdAt: createdAt, + authToken: auth.token!, + serverUrl: auth.serverUrl, + ); + if (onConsumptionFinished != null) { + _documentApi + .waitForConsumptionFinished(filename, title) + .then((value) => onConsumptionFinished(value)); + } + } + + @override + Future close() async { + for (final sub in _subs) { + await sub.cancel(); + } + return super.close(); + } +} diff --git a/lib/features/document_upload/cubit/document_upload_state.dart b/lib/features/document_upload/cubit/document_upload_state.dart new file mode 100644 index 0000000..189369e --- /dev/null +++ b/lib/features/document_upload/cubit/document_upload_state.dart @@ -0,0 +1,29 @@ +part of 'document_upload_cubit.dart'; + +@immutable +class DocumentUploadState extends Equatable { + final Map tags; + final Map correspondents; + final Map documentTypes; + + const DocumentUploadState({ + required this.tags, + required this.correspondents, + required this.documentTypes, + }); + + @override + List get props => []; + + DocumentUploadState copyWith({ + Map? tags, + Map? correspondents, + Map? documentTypes, + }) { + return DocumentUploadState( + tags: tags ?? this.tags, + correspondents: correspondents ?? this.correspondents, + documentTypes: documentTypes ?? this.documentTypes, + ); + } +} diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart new file mode 100644 index 0000000..80bc127 --- /dev/null +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -0,0 +1,275 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:intl/date_symbol_data_local.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/type/types.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; +import 'package:paperless_mobile/features/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/util.dart'; + +class DocumentUploadPreparationPage extends StatefulWidget { + final Uint8List fileBytes; + final String? title; + final String? filename; + final void Function(DocumentModel)? onSuccessfullyConsumed; + + const DocumentUploadPreparationPage({ + Key? key, + required this.fileBytes, + this.title, + this.filename, + this.onSuccessfullyConsumed, + }) : super(key: key); + + @override + State createState() => + _DocumentUploadPreparationPageState(); +} + +class _DocumentUploadPreparationPageState + extends State { + static const fkFileName = "filename"; + static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss"); + + final GlobalKey _formKey = GlobalKey(); + + PaperlessValidationErrors _errors = {}; + bool _isUploadLoading = false; + late bool _syncTitleAndFilename; + final _now = DateTime.now(); + + @override + void initState() { + super.initState(); + _syncTitleAndFilename = widget.filename == null && widget.title == null; + initializeDateFormatting(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + title: Text(S.of(context).documentsUploadPageTitle), + bottom: _isUploadLoading + ? const PreferredSize( + child: LinearProgressIndicator(), + preferredSize: Size.fromHeight(4.0)) + : null, + ), + 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: BlocBuilder( + builder: (context, state) { + return FormBuilder( + key: _formKey, + child: ListView( + children: [ + FormBuilderTextField( + autovalidateMode: AutovalidateMode.always, + name: DocumentModel.titleKey, + initialValue: + widget.title ?? "scan_${fileNameDateFormat.format(_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(""); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(""); + } + }, + ), + errorText: _errors[DocumentModel.titleKey], + ), + onChanged: (value) { + final String transformedValue = + _formatFilename(value ?? ''); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(transformedValue); + } + }, + ), + FormBuilderTextField( + autovalidateMode: AutovalidateMode.always, + readOnly: _syncTitleAndFilename, + enabled: !_syncTitleAndFilename, + name: fkFileName, + decoration: InputDecoration( + labelText: S.of(context).documentUploadFileNameLabel, + suffixText: ".pdf", + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => _formKey.currentState?.fields[fkFileName] + ?.didChange(''), + ), + ), + initialValue: widget.filename ?? + "scan_${fileNameDateFormat.format(_now)}", + ), + SwitchListTile( + value: _syncTitleAndFilename, + onChanged: (value) { + setState( + () => _syncTitleAndFilename = value, + ); + if (_syncTitleAndFilename) { + final String transformedValue = _formatFilename(_formKey + .currentState + ?.fields[DocumentModel.titleKey] + ?.value as String); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(transformedValue); + } + } + }, + title: Text(S + .of(context) + .documentUploadPageSynchronizeTitleAndFilenameLabel), //TODO: INTL + ), + 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 + " *", + ), + ), + LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialName) => + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + child: AddDocumentTypePage(initialName: initialName), + ), + label: S.of(context).documentDocumentTypePropertyLabel + " *", + name: DocumentModel.documentTypeKey, + state: state.documentTypes, + queryParameterIdBuilder: DocumentTypeQuery.fromId, + queryParameterNotAssignedBuilder: + DocumentTypeQuery.notAssigned, + prefixIcon: const Icon(Icons.description_outlined), + ), + LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialName) => + RepositoryProvider.value( + value: + RepositoryProvider.of>( + context, + ), + child: AddCorrespondentPage(initialName: initialName), + ), + label: + S.of(context).documentCorrespondentPropertyLabel + " *", + name: DocumentModel.correspondentKey, + state: state.correspondents, + queryParameterIdBuilder: CorrespondentQuery.fromId, + queryParameterNotAssignedBuilder: + CorrespondentQuery.notAssigned, + prefixIcon: const Icon(Icons.person_outline), + ), + const TagFormField( + name: DocumentModel.tagsKey, + notAssignedSelectable: false, + anyAssignedSelectable: false, + excludeAllowed: false, + //Label: "Tags" + " *", + ), + Text( + "* " + + S + .of(context) + .uploadPageAutomaticallInferredFieldsHintText, + style: Theme.of(context).textTheme.caption, + ), + ].padded(), + ), + ); + }, + ), + ); + } + + void _onSubmit() async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + final cubit = BlocProvider.of(context); + try { + setState(() => _isUploadLoading = true); + + final fv = _formKey.currentState!.value; + + final createdAt = fv[DocumentModel.createdKey] as DateTime?; + final title = fv[DocumentModel.titleKey] as String; + final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter; + final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery; + final correspondent = + fv[DocumentModel.correspondentKey] as IdQueryParameter; + + await cubit.upload( + widget.fileBytes, + filename: + _padWithPdfExtension(_formKey.currentState?.value[fkFileName]), + title: title, + onConsumptionFinished: widget.onSuccessfullyConsumed, + documentType: docType.id, + correspondent: correspondent.id, + tags: tags.ids, + createdAt: createdAt, + ); + showSnackBar(context, S.of(context).documentUploadSuccessText); + Navigator.pop(context, true); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } on PaperlessValidationErrors catch (PaperlessServerExceptions) { + setState(() => _errors = PaperlessServerExceptions); + } catch (unknownError, stackTrace) { + showErrorMessage( + context, const PaperlessServerException.unknown(), stackTrace); + } finally { + setState(() { + _isUploadLoading = false; + }); + } + } + } + + String _padWithPdfExtension(String source) { + return source.endsWith(".pdf") ? source : '$source.pdf'; + } + + String _formatFilename(String source) { + return source.replaceAll(RegExp(r"[\W_]"), "_"); + } +} diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index be59f59..abd1873 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -1,11 +1,9 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -@prod -@test -@lazySingleton +part 'documents_state.dart'; + class DocumentsCubit extends Cubit { final PaperlessDocumentsApi _api; diff --git a/lib/features/documents/bloc/documents_state.dart b/lib/features/documents/bloc/documents_state.dart index b016f9f..80ad056 100644 --- a/lib/features/documents/bloc/documents_state.dart +++ b/lib/features/documents/bloc/documents_state.dart @@ -1,5 +1,4 @@ -import 'package:equatable/equatable.dart'; -import 'package:paperless_api/paperless_api.dart'; +part of 'documents_cubit.dart'; class DocumentsState extends Equatable { final bool isLoaded; diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 6afb833..2e13b61 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -7,15 +7,15 @@ import 'package:flutter_form_builder/flutter_form_builder.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/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.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/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/view/pages/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'; @@ -57,147 +57,165 @@ class _DocumentEditPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton.extended( - onPressed: () async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - final values = _formKey.currentState!.value; - var updatedDocument = widget.document.copyWith( - title: values[fkTitle], - created: values[fkCreatedDate], - overwriteDocumentType: true, - documentType: (values[fkDocumentType] as IdQueryParameter).id, - overwriteCorrespondent: true, - correspondent: (values[fkCorrespondent] as IdQueryParameter).id, - overwriteStoragePath: true, - storagePath: (values[fkStoragePath] as IdQueryParameter).id, - overwriteTags: true, - tags: (values[fkTags] as IdsTagsQuery).includedIds, - ); - setState(() { - _isSubmitLoading = true; - }); - - try { - await widget.onEdit(updatedDocument); - showSnackBar(context, S.of(context).documentUpdateSuccessMessage); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } finally { - setState(() { - _isSubmitLoading = false; - }); - Navigator.pop(context); - } - } - }, - icon: const Icon(Icons.save), - label: Text(S.of(context).genericActionSaveLabel), - ), - appBar: AppBar( - title: Text(S.of(context).documentEditPageTitle), - 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, + return LabelsBlocProvider( + child: Scaffold( + resizeToAvoidBottomInset: false, + floatingActionButton: FloatingActionButton.extended( + onPressed: _onSubmit, + icon: const Icon(Icons.save), + label: Text(S.of(context).genericActionSaveLabel), ), - child: FormBuilder( - key: _formKey, - child: ListView(children: [ - _buildTitleFormField().padded(), - _buildCreatedAtFormField().padded(), - BlocBuilder>( - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (currentInput) => - BlocProvider.value( - value: BlocProvider.of(context), - child: AddDocumentTypePage( - initialName: currentInput, - ), - ), - label: S.of(context).documentDocumentTypePropertyLabel, - initialValue: - DocumentTypeQuery.fromId(widget.document.documentType), - state: state.labels, - name: fkDocumentType, - queryParameterIdBuilder: DocumentTypeQuery.fromId, - queryParameterNotAssignedBuilder: - DocumentTypeQuery.notAssigned, - prefixIcon: const Icon(Icons.description_outlined), - ); - }, - ).padded(), - BlocBuilder>( - 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, - state: state.labels, - initialValue: - CorrespondentQuery.fromId(widget.document.correspondent), - name: fkCorrespondent, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: - CorrespondentQuery.notAssigned, - prefixIcon: const Icon(Icons.person_outlined), - ); - }, - ).padded(), - BlocBuilder>( - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => - BlocProvider.value( - value: BlocProvider.of(context), - child: AddStoragePathPage(initalValue: initialValue), - ), - label: S.of(context).documentStoragePathPropertyLabel, - state: state.labels, - initialValue: - StoragePathQuery.fromId(widget.document.storagePath), - name: fkStoragePath, - queryParameterIdBuilder: StoragePathQuery.fromId, - queryParameterNotAssignedBuilder: - StoragePathQuery.notAssigned, - prefixIcon: const Icon(Icons.folder_outlined), - ); - }, - ).padded(), - TagFormField( - initialValue: IdsTagsQuery.included(widget.document.tags), - notAssignedSelectable: false, - anyAssignedSelectable: false, - excludeAllowed: false, - name: fkTags, - ).padded(), - ]), + appBar: AppBar( + title: Text(S.of(context).documentEditPageTitle), + 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().padded(), + _buildCreatedAtFormField().padded(), + _buildDocumentTypeFormField().padded(), + _buildCorrespondentFormField().padded(), + _buildStoragePathFormField().padded(), + TagFormField( + initialValue: IdsTagsQuery.included(widget.document.tags), + notAssignedSelectable: false, + anyAssignedSelectable: false, + excludeAllowed: false, + name: fkTags, + ).padded(), + ]), + ), ), ), ); } + BlocBuilder, LabelState> + _buildStoragePathFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + 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: state.labels, + initialValue: StoragePathQuery.fromId(widget.document.storagePath), + name: fkStoragePath, + queryParameterIdBuilder: StoragePathQuery.fromId, + queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, + prefixIcon: const Icon(Icons.folder_outlined), + ); + }, + ); + } + + BlocBuilder, LabelState> + _buildCorrespondentFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialValue) => + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + child: AddCorrespondentPage(initialName: initialValue), + ), + label: S.of(context).documentCorrespondentPropertyLabel, + state: state.labels, + initialValue: + CorrespondentQuery.fromId(widget.document.correspondent), + name: fkCorrespondent, + queryParameterIdBuilder: CorrespondentQuery.fromId, + queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, + prefixIcon: const Icon(Icons.person_outlined), + ); + }, + ); + } + + BlocBuilder, LabelState> + _buildDocumentTypeFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (currentInput) => + RepositoryProvider.value( + value: RepositoryProvider.of>( + context, + ), + child: AddDocumentTypePage( + initialName: currentInput, + ), + ), + label: S.of(context).documentDocumentTypePropertyLabel, + initialValue: DocumentTypeQuery.fromId(widget.document.documentType), + state: state.labels, + name: fkDocumentType, + queryParameterIdBuilder: DocumentTypeQuery.fromId, + queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, + prefixIcon: const Icon(Icons.description_outlined), + ); + }, + ); + } + + Future _onSubmit() async { + if (_formKey.currentState?.saveAndValidate() ?? false) { + final values = _formKey.currentState!.value; + var updatedDocument = widget.document.copyWith( + title: values[fkTitle], + created: values[fkCreatedDate], + overwriteDocumentType: true, + documentType: (values[fkDocumentType] as IdQueryParameter).id, + overwriteCorrespondent: true, + correspondent: (values[fkCorrespondent] as IdQueryParameter).id, + overwriteStoragePath: true, + storagePath: (values[fkStoragePath] as IdQueryParameter).id, + overwriteTags: true, + tags: (values[fkTags] as IdsTagsQuery).includedIds, + ); + setState(() { + _isSubmitLoading = true; + }); + + try { + await widget.onEdit(updatedDocument); + showSnackBar(context, S.of(context).documentUpdateSuccessMessage); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } finally { + setState(() { + _isSubmitLoading = false; + }); + Navigator.pop(context); + } + } + } + Widget _buildTitleFormField() { return FormBuilderTextField( name: fkTitle, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index fd8beeb..4b02c7f 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -3,11 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; @@ -15,11 +16,10 @@ import 'package:paperless_mobile/features/documents/view/widgets/search/document import 'package:paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; @@ -132,9 +132,20 @@ class _DocumentsPageState extends State { ), body: _buildBody(connectivityState), color: Theme.of(context).scaffoldBackgroundColor, - panelBuilder: (scrollController) => DocumentFilterPanel( - panelController: _filterPanelController, - scrollController: scrollController, + panelBuilder: (scrollController) => + BlocBuilder( + builder: (context, state) { + return LabelsBlocProvider( + child: DocumentFilterPanel( + panelController: _filterPanelController, + scrollController: scrollController, + initialFilter: state.filter, + onFilterChanged: (filter) => + BlocProvider.of(context) + .updateFilter(filter: filter), + ), + ); + }, ), ), ); @@ -192,21 +203,46 @@ class _DocumentsPageState extends State { onRefresh: _onRefresh, child: CustomScrollView( slivers: [ - DocumentsPageAppBar( - actions: [ - const SortDocumentsButton(), - IconButton( - icon: Icon( - settings.preferredViewType == ViewType.grid - ? Icons.list - : Icons.grid_view, - ), - onPressed: () => - BlocProvider.of(context) - .setViewType( - settings.preferredViewType.toggle()), + BlocProvider( + create: (context) => SavedViewCubit( + RepositoryProvider.of(context)), + child: BlocListener( + listener: (context, state) { + final documentsCubit = + BlocProvider.of(context); + try { + if (state.selectedSavedViewId == null) { + documentsCubit.updateFilter(); + } else { + final newFilter = state + .value[state.selectedSavedViewId] + ?.toDocumentFilter(); + if (newFilter != null) { + documentsCubit.updateFilter(filter: newFilter); + } + } + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + }, + child: DocumentsPageAppBar( + actions: [ + const SortDocumentsButton(), + IconButton( + icon: Icon( + settings.preferredViewType == ViewType.grid + ? Icons.list + : Icons.grid_view, + ), + onPressed: () => + BlocProvider.of( + context) + .setViewType( + settings.preferredViewType.toggle()), + ), + ], ), - ], + ), ), child, SliverToBoxAdapter( @@ -233,29 +269,11 @@ class _DocumentsPageState extends State { MaterialPageRoute _buildDetailsPageRoute( DocumentModel document) { return MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: BlocProvider.of(context), - ), - BlocProvider.value( - value: BlocProvider.of(context), - ), - BlocProvider.value( - value: BlocProvider.of(context), - ), - BlocProvider.value( - value: BlocProvider.of(context), - ), - BlocProvider.value( - value: BlocProvider.of(context), - ), - BlocProvider.value( - value: - DocumentDetailsCubit(getIt(), document), - ), - ], - child: const DocumentDetailsPage(), + builder: (_) => BlocProvider.value( + value: DocumentDetailsCubit(getIt(), document), + child: const LabelRepositoriesProvider( + child: DocumentDetailsPage(), + ), ), ); } diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index 60f47e9..971697b 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -4,8 +4,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/empty_state.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class DocumentsEmptyState extends StatelessWidget { diff --git a/lib/features/documents/view/widgets/grid/document_grid.dart b/lib/features/documents/view/widgets/grid/document_grid.dart index bf0ca40..16a532d 100644 --- a/lib/features/documents/view/widgets/grid/document_grid.dart +++ b/lib/features/documents/view/widgets/grid/document_grid.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; diff --git a/lib/features/documents/view/widgets/list/document_list.dart b/lib/features/documents/view/widgets/list/document_list.dart index f8fb268..2dcae35 100644 --- a/lib/features/documents/view/widgets/list/document_list.dart +++ b/lib/features/documents/view/widgets/list/document_list.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -34,21 +36,23 @@ class DocumentListView extends StatelessWidget { builderDelegate: PagedChildBuilderDelegate( animateTransitions: true, itemBuilder: (context, document, index) { - return DocumentListItem( - isLabelClickable: isLabelClickable, - document: document, - onTap: onTap, - isSelected: state.selection.contains(document), - onSelected: onSelected, - isAtLeastOneSelected: state.selection.isNotEmpty, - isTagSelectedPredicate: (int tagId) { - return state.filter.tags is IdsTagsQuery - ? (state.filter.tags as IdsTagsQuery) - .includedIds - .contains(tagId) - : false; - }, - onTagSelected: onTagSelected, + return LabelRepositoriesProvider( + child: DocumentListItem( + isLabelClickable: isLabelClickable, + document: document, + onTap: onTap, + isSelected: state.selection.contains(document), + onSelected: onSelected, + isAtLeastOneSelected: state.selection.isNotEmpty, + isTagSelectedPredicate: (int tagId) { + return state.filter.tags is IdsTagsQuery + ? (state.filter.tags as IdsTagsQuery) + .includedIds + .contains(tagId) + : false; + }, + onTagSelected: onTagSelected, + ), ); }, noItemsFoundIndicatorBuilder: (context) => hasInternetConnection 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 0bbc980..e29f625 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -4,15 +4,12 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:intl/intl.dart'; import 'package:paperless_mobile/util.dart'; @@ -24,10 +21,15 @@ class DocumentFilterPanel extends StatefulWidget { final PanelController panelController; final ScrollController scrollController; + final DocumentFilter initialFilter; + + final void Function(DocumentFilter filter) onFilterChanged; const DocumentFilterPanel({ Key? key, required this.panelController, required this.scrollController, + required this.onFilterChanged, + required this.initialFilter, }) : super(key: key); @override @@ -63,93 +65,84 @@ class _DocumentFilterPanelState extends State { 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: Column( + child: FormBuilder( + key: _formKey, + child: Column( + children: [ + Stack( + alignment: Alignment.center, children: [ - Stack( - alignment: Alignment.center, - children: [ - _buildDragLine(), - Align( - alignment: Alignment.topRight, - child: TextButton.icon( - icon: const Icon(Icons.refresh), - label: Text( - S.of(context).documentsFilterPageResetFilterLabel), - onPressed: () => _resetFilter(context), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).documentsFilterPageTitle, - style: Theme.of(context).textTheme.titleLarge, - ), - TextButton( - onPressed: _onApplyFilter, - child: Text( - S.of(context).documentsFilterPageApplyFilterLabel), - ), - ], - ).padded(), - const SizedBox( - height: 16.0, - ), - Expanded( - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), - child: ListView( - controller: widget.scrollController, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - S.of(context).documentsFilterPageSearchLabel), - ).padded(const EdgeInsets.only(left: 8.0)), - _buildQueryFormField(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, - allowCreation: false, - ).padded(), - // Required in order for the storage path field to be visible when typing - const SizedBox( - height: 150, - ), - ], - ).padded(), + _buildDragLine(), + Align( + alignment: Alignment.topRight, + child: TextButton.icon( + icon: const Icon(Icons.refresh), + label: + Text(S.of(context).documentsFilterPageResetFilterLabel), + onPressed: () => _resetFilter(context), ), ), ], ), - ); - }, + const SizedBox( + height: 8.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).documentsFilterPageTitle, + style: Theme.of(context).textTheme.titleLarge, + ), + TextButton( + onPressed: _onApplyFilter, + child: + Text(S.of(context).documentsFilterPageApplyFilterLabel), + ), + ], + ).padded(), + const SizedBox( + height: 16.0, + ), + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + child: ListView( + controller: widget.scrollController, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text(S.of(context).documentsFilterPageSearchLabel), + ).padded(const EdgeInsets.only(left: 8.0)), + _buildQueryFormField(), + Align( + alignment: Alignment.centerLeft, + child: + Text(S.of(context).documentsFilterPageAdvancedLabel), + ).padded(const EdgeInsets.only(left: 8.0, top: 8.0)), + _buildCreatedDateRangePickerFormField().padded(), + _buildAddedDateRangePickerFormField().padded(), + _buildCorrespondentFormField().padded(), + _buildDocumentTypeFormField().padded(), + _buildStoragePathFormField().padded(), + TagFormField( + name: DocumentModel.tagsKey, + initialValue: widget.initialFilter.tags, + allowCreation: false, + ).padded(), + // Required in order for the storage path field to be visible when typing + const SizedBox( + height: 150, + ), + ], + ).padded(), + ), + ), + ], + ), ), ); } @@ -163,15 +156,16 @@ class _DocumentFilterPanelState extends State { } } - Widget _buildDocumentTypeFormField(DocumentsState docState) { - return BlocBuilder>( + //TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField! + Widget _buildDocumentTypeFormField() { + return BlocBuilder, LabelState>( builder: (context, state) { return LabelFormField( formBuilderState: _formKey.currentState, name: fkDocumentType, state: state.labels, label: S.of(context).documentDocumentTypePropertyLabel, - initialValue: docState.filter.documentType, + initialValue: widget.initialFilter.documentType, queryParameterIdBuilder: DocumentTypeQuery.fromId, queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, prefixIcon: const Icon(Icons.description_outlined), @@ -180,15 +174,15 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildCorrespondentFormField(DocumentsState docState) { - return BlocBuilder>( + Widget _buildCorrespondentFormField() { + return BlocBuilder, LabelState>( builder: (context, state) { return LabelFormField( formBuilderState: _formKey.currentState, name: fkCorrespondent, state: state.labels, label: S.of(context).documentCorrespondentPropertyLabel, - initialValue: docState.filter.correspondent, + initialValue: widget.initialFilter.correspondent, queryParameterIdBuilder: CorrespondentQuery.fromId, queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, prefixIcon: const Icon(Icons.person_outline), @@ -197,15 +191,15 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildStoragePathFormField(DocumentsState docState) { - return BlocBuilder>( + Widget _buildStoragePathFormField() { + return BlocBuilder, LabelState>( builder: (context, state) { return LabelFormField( formBuilderState: _formKey.currentState, name: fkStoragePath, state: state.labels, label: S.of(context).documentStoragePathPropertyLabel, - initialValue: docState.filter.storagePath, + initialValue: widget.initialFilter.storagePath, queryParameterIdBuilder: StoragePathQuery.fromId, queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, prefixIcon: const Icon(Icons.folder_outlined), @@ -214,7 +208,7 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildQueryFormField(DocumentsState state) { + Widget _buildQueryFormField() { final queryType = _formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ?? QueryType.titleAndContent; @@ -239,16 +233,15 @@ class _DocumentFilterPanelState extends State { prefixIcon: const Icon(Icons.search_outlined), labelText: label, suffixIcon: QueryTypeFormField( - initialValue: state.filter.queryType, + initialValue: widget.initialFilter.queryType, afterSelected: (queryType) => setState(() {}), ), ), - initialValue: state.filter.queryText, + initialValue: widget.initialFilter.queryText, ).padded(); } - Widget _buildDateRangePickerHelper( - DocumentsState state, String formFieldKey) { + Widget _buildDateRangePickerHelper(String formFieldKey) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -328,13 +321,13 @@ class _DocumentFilterPanelState extends State { ); } - Widget _buildCreatedDateRangePickerFormField(DocumentsState state) { + Widget _buildCreatedDateRangePickerFormField() { return Column( children: [ FormBuilderDateRangePicker( initialValue: _dateTimeRangeOfNullable( - state.filter.createdDateAfter, - state.filter.createdDateBefore, + widget.initialFilter.createdDateAfter, + widget.initialFilter.createdDateBefore, ), // Workaround for theme data not being correctly passed to daterangepicker, see // https://github.com/flutter/flutter/issues/87580 @@ -371,18 +364,18 @@ class _DocumentFilterPanelState extends State { ), ), const SizedBox(height: 4.0), - _buildDateRangePickerHelper(state, fkCreatedAt), + _buildDateRangePickerHelper(fkCreatedAt), ], ); } - Widget _buildAddedDateRangePickerFormField(DocumentsState state) { + Widget _buildAddedDateRangePickerFormField() { return Column( children: [ FormBuilderDateRangePicker( initialValue: _dateTimeRangeOfNullable( - state.filter.addedDateAfter, - state.filter.addedDateBefore, + widget.initialFilter.addedDateAfter, + widget.initialFilter.addedDateBefore, ), // Workaround for theme data not being correctly passed to daterangepicker, see // https://github.com/flutter/flutter/issues/87580 @@ -419,7 +412,7 @@ class _DocumentFilterPanelState extends State { ), ), const SizedBox(height: 4.0), - _buildDateRangePickerHelper(state, fkAddedAt), + _buildDateRangePickerHelper(fkAddedAt), ], ); } diff --git a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart index af714b4..43c426f 100644 --- a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart +++ b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; class SortFieldSelectionBottomSheet extends StatefulWidget { - const SortFieldSelectionBottomSheet({super.key}); + final SortOrder initialSortOrder; + final SortField initialSortField; + + final Future Function(SortField field, SortOrder order) onSubmit; + + const SortFieldSelectionBottomSheet({ + super.key, + required this.initialSortOrder, + required this.initialSortField, + required this.onSubmit, + }); @override State createState() => @@ -17,81 +23,60 @@ class SortFieldSelectionBottomSheet extends StatefulWidget { class _SortFieldSelectionBottomSheetState extends State { - SortField? _selectedFieldLoading; - SortOrder? _selectedOrderLoading; + late SortField _currentSortField; + late SortOrder _currentSortOrder; + + @override + void initState() { + super.initState(); + _currentSortField = widget.initialSortField; + _currentSortOrder = widget.initialSortOrder; + } @override Widget build(BuildContext context) { return ClipRRect( - child: BlocBuilder( - bloc: getIt(), - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( S.of(context).documentsPageOrderByLabel, style: Theme.of(context).textTheme.caption, textAlign: TextAlign.start, ).padded( - const EdgeInsets.symmetric(horizontal: 16, vertical: 16)), - Column( - children: SortField.values - .map( - (e) => _buildSortOption( - e, - state.filter.sortOrder, - state.filter.sortField == e, - _selectedFieldLoading == e, - ), - ) - .toList(), + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + TextButton( + child: Text(S.of(context).documentsFilterPageApplyFilterLabel), + onPressed: () => widget.onSubmit( + _currentSortField, + _currentSortOrder, + ), ), ], - ); - }, + ), + Column( + children: SortField.values.map(_buildSortOption).toList(), + ), + ], ), ); } Widget _buildSortOption( SortField field, - SortOrder order, - bool isCurrentlySelected, - bool isNextSelected, ) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 32), title: Text( _localizedSortField(field), ), - trailing: isNextSelected - ? (_buildOrderIcon(_selectedOrderLoading!)) - : (_selectedOrderLoading == null && isCurrentlySelected - ? _buildOrderIcon(order) - : null), - onTap: () async { - setState(() { - _selectedFieldLoading = field; - _selectedOrderLoading = - isCurrentlySelected ? order.toggle() : SortOrder.descending; - }); - BlocProvider.of(context) - .updateCurrentFilter((filter) => filter.copyWith( - sortOrder: isCurrentlySelected - ? order.toggle() - : SortOrder.descending, - sortField: field, - )) - .whenComplete(() { - if (mounted) { - setState(() { - _selectedFieldLoading = null; - _selectedOrderLoading = null; - }); - } - }); - }, + trailing: _currentSortField == field + ? _buildOrderIcon(_currentSortOrder) + : null, ); } diff --git a/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart b/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart index 34c8f0d..74657ed 100644 --- a/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart +++ b/lib/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class BulkDeleteConfirmationDialog extends StatelessWidget { diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index ad09ea6..6da9fb3 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; @@ -79,7 +80,6 @@ class _DocumentsPageAppBarState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ -//TODO: replace with sorting stuff... SavedViewSelectionWidget(height: 48, enabled: enabled), ], ), diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index de56c46..7bb61d1 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart'; @@ -33,11 +32,19 @@ class _SortDocumentsButtonState extends State { topRight: Radius.circular(16), ), ), - builder: (context) => BlocProvider.value( - value: getIt(), - child: const FractionallySizedBox( - heightFactor: .6, - child: SortFieldSelectionBottomSheet(), + builder: (context) => FractionallySizedBox( + heightFactor: .6, + child: BlocBuilder( + builder: (context, state) { + return SortFieldSelectionBottomSheet( + initialSortField: state.filter.sortField, + initialSortOrder: state.filter.sortOrder, + onSubmit: (field, order) => + BlocProvider.of(context).updateCurrentFilter( + (filter) => filter.copyWith(sortField: field, sortOrder: order), + ), + ); + }, ), ), ); diff --git a/lib/features/edit_label/cubit/edit_label_cubit.dart b/lib/features/edit_label/cubit/edit_label_cubit.dart new file mode 100644 index 0000000..39ec63b --- /dev/null +++ b/lib/features/edit_label/cubit/edit_label_cubit.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart'; + +class EditLabelCubit extends Cubit> { + final LabelRepository _repository; + + StreamSubscription>? _subscription; + + EditLabelCubit(LabelRepository repository) + : _repository = repository, + super(const EditLabelInitial()) { + _subscription = _repository.labels + .listen((labels) => emit(EditLabelState(labels: labels))); + } + + Future create(T label) => _repository.create(label); + + Future update(T label) => _repository.update(label); + + Future delete(T label) => _repository.delete(label); + + @override + Future close() { + _subscription?.cancel(); + return super.close(); + } +} diff --git a/lib/features/edit_label/cubit/edit_label_state.dart b/lib/features/edit_label/cubit/edit_label_state.dart new file mode 100644 index 0000000..e26f28c --- /dev/null +++ b/lib/features/edit_label/cubit/edit_label_state.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/widgets.dart'; + +@immutable +class EditLabelState extends Equatable { + final Map labels; + + const EditLabelState({required this.labels}); + + @override + List get props => [labels]; +} + +class EditLabelInitial extends EditLabelState { + const EditLabelInitial() : super(labels: const {}); +} diff --git a/lib/features/edit_label/view/add_label_page.dart b/lib/features/edit_label/view/add_label_page.dart new file mode 100644 index 0000000..25e71d6 --- /dev/null +++ b/lib/features/edit_label/view/add_label_page.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class AddLabelPage extends StatelessWidget { + final String? initialName; + final Widget pageTitle; + final T Function(Map json) fromJsonT; + final List additionalFields; + + const AddLabelPage({ + super.key, + this.initialName, + required this.pageTitle, + required this.fromJsonT, + this.additionalFields = const [], + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: AddLabelFormWidget( + pageTitle: pageTitle, + label: fromJsonT({'name': initialName}), + additionalFields: additionalFields, + fromJsonT: fromJsonT, + ), + ); + } +} + +class AddLabelFormWidget extends StatelessWidget { + final T? label; + final T Function(Map json) fromJsonT; + final List additionalFields; + + final Widget pageTitle; + const AddLabelFormWidget({ + super.key, + this.label, + required this.fromJsonT, + required this.additionalFields, + required this.pageTitle, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: pageTitle, + ), + body: LabelForm( + initialValue: label, + fromJsonT: fromJsonT, + submitButtonConfig: SubmitButtonConfig( + icon: const Icon(Icons.add), + label: Text(S.of(context).genericActionCreateLabel), + onSubmit: BlocProvider.of>(context).create, + ), + additionalFields: additionalFields, + ), + ); + } +} diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart new file mode 100644 index 0000000..5e3ac0b --- /dev/null +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class EditLabelPage extends StatelessWidget { + final T label; + final T Function(Map json) fromJsonT; + final List additionalFields; + + const EditLabelPage({ + super.key, + required this.label, + required this.fromJsonT, + this.additionalFields = const [], + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: EditLabelForm( + label: label, + additionalFields: additionalFields, + fromJsonT: fromJsonT, + ), + ); + } +} + +class EditLabelForm extends StatelessWidget { + final T label; + final T Function(Map json) fromJsonT; + final List additionalFields; + + const EditLabelForm({ + super.key, + required this.label, + required this.fromJsonT, + required this.additionalFields, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).genericActionEditLabel), + actions: [ + IconButton( + onPressed: () => _onDelete(context), + icon: const Icon(Icons.delete), + ), + ], + ), + body: LabelForm( + initialValue: label, + fromJsonT: fromJsonT, + submitButtonConfig: SubmitButtonConfig( + icon: const Icon(Icons.update), + label: Text(S.of(context).genericActionUpdateLabel), + onSubmit: BlocProvider.of>(context).update, + ), + additionalFields: additionalFields, + ), + ); + } + + void _onDelete(BuildContext context) { + if ((label.documentCount ?? 0) > 0) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(S.of(context).editLabelPageConfirmDeletionDialogTitle), + content: Text( + S.of(context).editLabelPageDeletionDialogText, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(S.of(context).genericActionCancelLabel), + ), + TextButton( + onPressed: () { + BlocProvider.of>(context).delete(label); + Navigator.pop(context); + }, + child: Text( + S.of(context).genericActionDeleteLabel, + style: TextStyle(color: Theme.of(context).errorColor), + ), + ), + ], + ), + ); + } else { + BlocProvider.of>(context).delete(label); + Navigator.pop(context); + } + } +} diff --git a/lib/features/edit_label/view/impl/add_correspondent_page.dart b/lib/features/edit_label/view/impl/add_correspondent_page.dart new file mode 100644 index 0000000..60b5615 --- /dev/null +++ b/lib/features/edit_label/view/impl/add_correspondent_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class AddCorrespondentPage extends StatelessWidget { + final String? initialName; + const AddCorrespondentPage({Key? key, this.initialName}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: AddLabelPage( + pageTitle: Text(S.of(context).addCorrespondentPageTitle), + fromJsonT: Correspondent.fromJson, + initialName: initialName, + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/add_document_type_page.dart b/lib/features/edit_label/view/impl/add_document_type_page.dart new file mode 100644 index 0000000..7652f45 --- /dev/null +++ b/lib/features/edit_label/view/impl/add_document_type_page.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class AddDocumentTypePage extends StatelessWidget { + final String? initialName; + const AddDocumentTypePage({ + super.key, + this.initialName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: AddLabelPage( + pageTitle: Text(S.of(context).addDocumentTypePageTitle), + fromJsonT: DocumentType.fromJson, + initialName: initialName, + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/add_storage_path_page.dart b/lib/features/edit_label/view/impl/add_storage_path_page.dart new file mode 100644 index 0000000..4a2bf31 --- /dev/null +++ b/lib/features/edit_label/view/impl/add_storage_path_page.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class AddStoragePathPage extends StatelessWidget { + final String? initalValue; + const AddStoragePathPage({Key? key, this.initalValue}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: AddLabelPage( + pageTitle: Text(S.of(context).addStoragePathPageTitle), + fromJsonT: StoragePath.fromJson, + initialName: initalValue, + additionalFields: const [ + StoragePathAutofillFormBuilderField(name: StoragePath.pathKey), + SizedBox(height: 120.0), + ], + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/add_tag_page.dart b/lib/features/edit_label/view/impl/add_tag_page.dart new file mode 100644 index 0000000..bd26dc4 --- /dev/null +++ b/lib/features/edit_label/view/impl/add_tag_page.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class AddTagPage extends StatelessWidget { + final String? initialValue; + const AddTagPage({Key? key, this.initialValue}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: AddLabelPage( + pageTitle: Text(S.of(context).addTagPageTitle), + fromJsonT: Tag.fromJson, + initialName: initialValue, + additionalFields: [ + FormBuilderColorPickerField( + name: Tag.colorKey, + valueTransformer: (color) => "#${color?.value.toRadixString(16)}", + decoration: InputDecoration( + label: Text(S.of(context).tagColorPropertyLabel), + ), + colorPickerType: ColorPickerType.materialPicker, + initialValue: null, + ), + FormBuilderCheckbox( + name: Tag.isInboxTagKey, + title: Text(S.of(context).tagInboxTagPropertyLabel), + ), + ], + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/edit_correspondent_page.dart b/lib/features/edit_label/view/impl/edit_correspondent_page.dart new file mode 100644 index 0000000..3fd8d42 --- /dev/null +++ b/lib/features/edit_label/view/impl/edit_correspondent_page.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; +import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class EditCorrespondentPage extends StatelessWidget { + final Correspondent correspondent; + const EditCorrespondentPage({super.key, required this.correspondent}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => EditLabelCubit( + RepositoryProvider.of>(context), + ), + child: EditLabelPage( + label: correspondent, + fromJsonT: Correspondent.fromJson, + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/edit_document_type_page.dart b/lib/features/edit_label/view/impl/edit_document_type_page.dart new file mode 100644 index 0000000..7556027 --- /dev/null +++ b/lib/features/edit_label/view/impl/edit_document_type_page.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class EditDocumentTypePage extends StatelessWidget { + final DocumentType documentType; + const EditDocumentTypePage({super.key, required this.documentType}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: EditLabelPage( + label: documentType, + fromJsonT: DocumentType.fromJson, + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/edit_storage_path_page.dart b/lib/features/edit_label/view/impl/edit_storage_path_page.dart new file mode 100644 index 0000000..abc9f04 --- /dev/null +++ b/lib/features/edit_label/view/impl/edit_storage_path_page.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; + +class EditStoragePathPage extends StatelessWidget { + final StoragePath storagePath; + const EditStoragePathPage({super.key, required this.storagePath}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: EditLabelPage( + label: storagePath, + fromJsonT: StoragePath.fromJson, + additionalFields: [ + StoragePathAutofillFormBuilderField( + name: StoragePath.pathKey, + initialValue: storagePath.path, + ), + const SizedBox(height: 120.0), + ], + ), + ); + } +} diff --git a/lib/features/edit_label/view/impl/edit_tag_page.dart b/lib/features/edit_label/view/impl/edit_tag_page.dart new file mode 100644 index 0000000..97601e2 --- /dev/null +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; + +class EditTagPage extends StatelessWidget { + final Tag tag; + + const EditTagPage({super.key, required this.tag}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: EditLabelPage( + label: tag, + fromJsonT: Tag.fromJson, + additionalFields: [ + FormBuilderColorPickerField( + initialValue: tag.color, + name: Tag.colorKey, + decoration: InputDecoration( + label: Text(S.of(context).tagColorPropertyLabel), + ), + colorPickerType: ColorPickerType.blockPicker, + ), + FormBuilderCheckbox( + initialValue: tag.isInboxTag, + name: Tag.isInboxTagKey, + title: Text(S.of(context).tagInboxTagPropertyLabel), + ), + ], + ), + ); + } +} diff --git a/lib/features/labels/view/pages/edit_label_page.dart b/lib/features/edit_label/view/label_form.dart similarity index 61% rename from lib/features/labels/view/pages/edit_label_page.dart rename to lib/features/edit_label/view/label_form.dart index c75a75d..f84cc9f 100644 --- a/lib/features/labels/view/pages/edit_label_page.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -9,27 +7,42 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; -class EditLabelPage extends StatefulWidget { - final T label; +class SubmitButtonConfig { + final Widget icon; + final Widget label; final Future Function(T) onSubmit; - final Future Function(T) onDelete; - final T Function(JSON) fromJson; + + SubmitButtonConfig({ + required this.icon, + required this.label, + required this.onSubmit, + }); +} + +class LabelForm extends StatefulWidget { + final T? initialValue; + + final SubmitButtonConfig submitButtonConfig; + + /// FromJson method to parse the form field values into a label instance. + final T Function(Map json) fromJsonT; + + /// List of additionally rendered form fields. final List additionalFields; - const EditLabelPage({ + const LabelForm({ Key? key, - required this.label, - required this.fromJson, - required this.onSubmit, - required this.onDelete, + required this.initialValue, + required this.fromJsonT, this.additionalFields = const [], + required this.submitButtonConfig, }) : super(key: key); @override - State createState() => _EditLabelPageState(); + State createState() => _LabelFormState(); } -class _EditLabelPageState extends State> { +class _LabelFormState extends State> { final _formKey = GlobalKey(); PaperlessValidationErrors _errors = {}; @@ -38,18 +51,9 @@ class _EditLabelPageState extends State> { Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Text(S.of(context).genericActionEditLabel), - actions: [ - IconButton( - onPressed: _onDelete, - icon: const Icon(Icons.delete), - ), - ], - ), floatingActionButton: FloatingActionButton.extended( - icon: const Icon(Icons.update), - label: Text(S.of(context).genericActionUpdateLabel), + icon: widget.submitButtonConfig.icon, + label: widget.submitButtonConfig.label, onPressed: _onSubmit, ), body: FormBuilder( @@ -63,7 +67,7 @@ class _EditLabelPageState extends State> { errorText: _errors[Label.nameKey], ), validator: FormBuilderValidators.required(), - initialValue: widget.label.name, + initialValue: widget.initialValue?.name, onChanged: (val) => setState(() => _errors = {}), ), FormBuilderTextField( @@ -72,12 +76,13 @@ class _EditLabelPageState extends State> { labelText: S.of(context).labelMatchPropertyLabel, errorText: _errors[Label.matchKey], ), - initialValue: widget.label.match, + initialValue: widget.initialValue?.match, onChanged: (val) => setState(() => _errors = {}), ), FormBuilderDropdown( + //TODO: Extract to own widget. name: Label.matchingAlgorithmKey, - initialValue: widget.label.matchingAlgorithm?.value ?? + initialValue: widget.initialValue?.matchingAlgorithm?.value ?? MatchingAlgorithm.allWords.value, decoration: InputDecoration( labelText: S.of(context).labelMatchingAlgorithmPropertyLabel, @@ -95,7 +100,7 @@ class _EditLabelPageState extends State> { ), FormBuilderCheckbox( name: Label.isInsensitiveKey, - initialValue: widget.label.isInsensitive, + initialValue: widget.initialValue?.isInsensitive, title: Text(S.of(context).labelIsInsensivitePropertyLabel), ), ...widget.additionalFields, @@ -105,46 +110,14 @@ class _EditLabelPageState extends State> { ); } - void _onDelete() { - if ((widget.label.documentCount ?? 0) > 0) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(S.of(context).editLabelPageConfirmDeletionDialogTitle), - content: Text( - S.of(context).editLabelPageDeletionDialogText, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(S.of(context).genericActionCancelLabel), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - widget.onDelete(widget.label); - }, - child: Text( - S.of(context).genericActionDeleteLabel, - style: TextStyle(color: Theme.of(context).errorColor), - ), - ), - ], - ), - ); - } else { - widget.onDelete(widget.label); - } - } - void _onSubmit() async { if (_formKey.currentState?.saveAndValidate() ?? false) { try { final mergedJson = { - ...widget.label.toJson(), + ...widget.initialValue?.toJson() ?? {}, ..._formKey.currentState!.value }; - await widget.onSubmit(widget.fromJson(mergedJson)); + await widget.submitButtonConfig.onSubmit(widget.fromJsonT(mergedJson)); Navigator.pop(context); } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 01610d9..a699c30 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -3,18 +3,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; import 'package:paperless_mobile/util.dart'; @@ -59,17 +57,17 @@ class _HomePageState extends State { MultiBlocProvider( providers: [ BlocProvider.value( - value: getIt(), + value: DocumentsCubit(getIt()), ), ], child: const DocumentsPage(), ), BlocProvider.value( - value: getIt(), + value: DocumentScannerCubit(), child: const ScannerPage(), ), BlocProvider.value( - value: getIt(), + value: DocumentsCubit(getIt()), child: const LabelsPage(), ), ][_currentIndex], @@ -78,20 +76,17 @@ class _HomePageState extends State { ); } - Future _initializeData(BuildContext context) { + void _initializeData(BuildContext context) { try { - return Future.wait([ - BlocProvider.of(context) - .updateInformtion(), - getIt().initialize(), - getIt().initialize(), - getIt().initialize(), - getIt().initialize(), - getIt().initialize(), - ]); + RepositoryProvider.of>(context).findAll(); + RepositoryProvider.of>(context).findAll(); + RepositoryProvider.of>(context).findAll(); + RepositoryProvider.of>(context).findAll(); + RepositoryProvider.of(context).findAll(); + BlocProvider.of(context) + .updateInformtion(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); - return Future.error(error); } } } diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index b5184f9..00ec9a6 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -3,15 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; @@ -188,11 +187,14 @@ class InfoDrawer extends StatelessWidget { onTap: () { try { BlocProvider.of(context).logout(); - getIt().reset(); - getIt().reset(); - getIt().reset(); - getIt().reset(); - getIt().reset(); + RepositoryProvider.of>(context).clear(); + RepositoryProvider.of>(context) + .clear(); + RepositoryProvider.of>(context) + .clear(); + RepositoryProvider.of>(context) + .clear(); + RepositoryProvider.of(context).clear(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } @@ -208,13 +210,14 @@ class InfoDrawer extends StatelessWidget { Future _onOpenInbox(BuildContext context) async { await Navigator.of(context).push( MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: getIt()..loadInbox(), + builder: (_) => LabelRepositoriesProvider( + child: BlocProvider( + create: (context) => InboxCubit( + RepositoryProvider.of>(context), + getIt(), ), - ], - child: const InboxPage(), + child: const InboxPage(), + ), ), maintainState: false, ), diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index 5f086d9..a5e677e 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -1,20 +1,21 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; -@injectable class InboxCubit extends Cubit { - final PaperlessLabelsApi _labelApi; + final LabelRepository _tagsRepository; final PaperlessDocumentsApi _documentsApi; - InboxCubit(this._labelApi, this._documentsApi) : super(const InboxState()); + InboxCubit(this._tagsRepository, this._documentsApi) + : super(const InboxState()); /// /// Fetches inbox tag ids and loads the inbox items (documents). /// Future loadInbox() async { - final inboxTags = await _labelApi.getTags().then( + final inboxTags = await _tagsRepository.findAll().then( (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), ); if (inboxTags.isEmpty) { diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index d0c9d09..9413a18 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; class InboxItem extends StatelessWidget { @@ -49,18 +48,16 @@ class InboxItem extends StatelessWidget { onTap: () => Navigator.push( context, MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider( - create: (context) => DocumentDetailsCubit( - getIt(), - document, - ), + builder: (_) => BlocProvider.value( + value: DocumentDetailsCubit( + getIt(), + document, + ), + child: const LabelRepositoriesProvider( + child: DocumentDetailsPage( + allowEdit: false, + isLabelClickable: false, ), - ], - child: const DocumentDetailsPage( - allowEdit: false, - isLabelClickable: false, ), ), ), diff --git a/lib/features/labels/bloc/global_state_bloc_provider.dart b/lib/features/labels/bloc/global_state_bloc_provider.dart deleted file mode 100644 index 8b36cd3..0000000 --- a/lib/features/labels/bloc/global_state_bloc_provider.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; - -class GlobalStateBlocProvider extends StatelessWidget { - final List additionalProviders; - final Widget child; - const GlobalStateBlocProvider({ - super.key, - this.additionalProviders = const [], - required this.child, - }); - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), - ...additionalProviders, - ], - child: child, - ); - } -} diff --git a/lib/features/labels/bloc/label_cubit.dart b/lib/features/labels/bloc/label_cubit.dart index 3ad09ce..bee3ae1 100644 --- a/lib/features/labels/bloc/label_cubit.dart +++ b/lib/features/labels/bloc/label_cubit.dart @@ -1,59 +1,43 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -abstract class LabelCubit extends Cubit> { - final PaperlessLabelsApi labelsApi; +class LabelCubit extends Cubit> { + final LabelRepository _repository; - LabelCubit(this.labelsApi) : super(LabelState.initial()); + late StreamSubscription _subscription; - @protected - void loadFrom(Iterable items) { - emit( - LabelState( - isLoaded: true, - labels: Map.fromIterable(items, key: (e) => (e as T).id!), - ), + LabelCubit(this._repository) : super(LabelState.initial()) { + _subscription = _repository.labels.listen( + (update) => emit(LabelState(isLoaded: true, labels: update)), ); } + /// + /// Adds [item] to the current state. A new state is automatically pushed + /// due to the subscription to the repository, which updates the state on + /// operation. + /// Future add(T item) async { assert(item.id == null); - final addedItem = await save(item); - final newValues = {...state.labels}; - newValues.putIfAbsent(addedItem.id!, () => addedItem); - emit( - LabelState( - isLoaded: true, - labels: newValues, - ), - ); + final addedItem = await _repository.create(item); return addedItem; } Future replace(T item) async { assert(item.id != null); - final updatedItem = await update(item); - final updatedValues = {...state.labels}; - updatedValues[item.id!] = updatedItem; - emit( - LabelState( - isLoaded: state.isLoaded, - labels: updatedValues, - ), - ); + final updatedItem = await _repository.update(item); return updatedItem; } Future remove(T item) async { assert(item.id != null); if (state.labels.containsKey(item.id)) { - final deletedId = await delete(item); - final updatedValues = {...state.labels}..remove(deletedId); - emit( - LabelState(isLoaded: true, labels: updatedValues), - ); + await _repository.delete(item); } } @@ -61,14 +45,9 @@ abstract class LabelCubit extends Cubit> { emit(LabelState(isLoaded: false, labels: {})); } - Future initialize(); - - @protected - Future save(T item); - - @protected - Future update(T item); - - @protected - Future delete(T item); + @override + Future close() { + _subscription.cancel(); + return super.close(); + } } diff --git a/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart b/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart new file mode 100644 index 0000000..a290167 --- /dev/null +++ b/lib/features/labels/bloc/providers/correspondent_bloc_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class CorrespondentBlocProvider extends StatelessWidget { + final Widget child; + const CorrespondentBlocProvider({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: child, + ); + } +} diff --git a/lib/features/labels/bloc/providers/document_type_bloc_provider.dart b/lib/features/labels/bloc/providers/document_type_bloc_provider.dart new file mode 100644 index 0000000..be1dba2 --- /dev/null +++ b/lib/features/labels/bloc/providers/document_type_bloc_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class DocumentTypeBlocProvider extends StatelessWidget { + final Widget child; + const DocumentTypeBlocProvider({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: child, + ); + } +} diff --git a/lib/features/labels/bloc/providers/labels_bloc_provider.dart b/lib/features/labels/bloc/providers/labels_bloc_provider.dart new file mode 100644 index 0000000..47c4fa0 --- /dev/null +++ b/lib/features/labels/bloc/providers/labels_bloc_provider.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class LabelsBlocProvider extends StatelessWidget { + final Widget child; + const LabelsBlocProvider({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider>( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + ), + BlocProvider>( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + ), + BlocProvider>( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + ), + BlocProvider>( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + ), + ], + child: child, + ); + } +} diff --git a/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart b/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart new file mode 100644 index 0000000..5f6e16c --- /dev/null +++ b/lib/features/labels/bloc/providers/storage_path_bloc_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class StoragePathBlocProvider extends StatelessWidget { + final Widget child; + const StoragePathBlocProvider({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: child, + ); + } +} diff --git a/lib/features/labels/bloc/providers/tag_bloc_provider.dart b/lib/features/labels/bloc/providers/tag_bloc_provider.dart new file mode 100644 index 0000000..5e414f1 --- /dev/null +++ b/lib/features/labels/bloc/providers/tag_bloc_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; + +class TagBlocProvider extends StatelessWidget { + final Widget child; + const TagBlocProvider({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: child, + ); + } +} diff --git a/lib/features/labels/correspondent/bloc/correspondents_cubit.dart b/lib/features/labels/correspondent/bloc/correspondents_cubit.dart deleted file mode 100644 index b287987..0000000 --- a/lib/features/labels/correspondent/bloc/correspondents_cubit.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -import 'package:injectable/injectable.dart'; - -@prod -@test -@lazySingleton -class CorrespondentCubit extends LabelCubit { - CorrespondentCubit(super.metaDataService); - - @override - Future initialize() async { - return labelsApi.getCorrespondents().then(loadFrom); - } - - @override - Future save(Correspondent item) => - labelsApi.saveCorrespondent(item); - - @override - Future update(Correspondent item) => - labelsApi.updateCorrespondent(item); - - @override - Future delete(Correspondent item) => labelsApi.deleteCorrespondent(item); -} diff --git a/lib/features/labels/correspondent/view/pages/add_correspondent_page.dart b/lib/features/labels/correspondent/view/pages/add_correspondent_page.dart deleted file mode 100644 index 6ab6e03..0000000 --- a/lib/features/labels/correspondent/view/pages/add_correspondent_page.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; - -class AddCorrespondentPage extends StatelessWidget { - final String? initalValue; - const AddCorrespondentPage({Key? key, this.initalValue}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AddLabelPage( - addLabelStr: S.of(context).addCorrespondentPageTitle, - fromJson: Correspondent.fromJson, - cubit: BlocProvider.of(context), - initialName: initalValue, - ); - } -} diff --git a/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart b/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart deleted file mode 100644 index 644b041..0000000 --- a/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart'; -import 'package:paperless_mobile/util.dart'; - -class EditCorrespondentPage extends StatelessWidget { - final Correspondent correspondent; - const EditCorrespondentPage({super.key, required this.correspondent}); - - @override - Widget build(BuildContext context) { - return EditLabelPage( - label: correspondent, - onSubmit: BlocProvider.of(context).replace, - onDelete: (correspondent) => _onDelete(context, correspondent), - fromJson: Correspondent.fromJson, - ); - } - - Future _onDelete( - BuildContext context, - Correspondent correspondent, - ) async { - try { - await BlocProvider.of(context).remove(correspondent); - final cubit = BlocProvider.of(context); - if (cubit.state.filter.correspondent.id == correspondent.id) { - await cubit.updateCurrentFilter( - (filter) => filter.copyWith( - correspondent: const CorrespondentQuery.unset(), - ), - ); - } - Navigator.pop(context); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } -} diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 986cbe7..296a0ec 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/util.dart'; @@ -22,22 +23,25 @@ class CorrespondentWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return AbsorbPointer( - absorbing: !isClickable, - child: BlocBuilder>( - builder: (context, state) { - return GestureDetector( - onTap: () => _addCorrespondentToFilter(context), - child: Text( - (state.getLabel(correspondentId)?.name) ?? "-", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2?.copyWith( - color: textColor ?? Theme.of(context).colorScheme.primary, - ), - ), - ); - }, + return CorrespondentBlocProvider( + child: AbsorbPointer( + absorbing: !isClickable, + child: + BlocBuilder, LabelState>( + builder: (context, state) { + return GestureDetector( + onTap: () => _addCorrespondentToFilter(context), + child: Text( + (state.getLabel(correspondentId)?.name) ?? "-", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2?.copyWith( + color: textColor ?? Theme.of(context).colorScheme.primary, + ), + ), + ); + }, + ), ), ); } diff --git a/lib/features/labels/document_type/bloc/document_type_cubit.dart b/lib/features/labels/document_type/bloc/document_type_cubit.dart deleted file mode 100644 index 6c9e978..0000000 --- a/lib/features/labels/document_type/bloc/document_type_cubit.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -import 'package:injectable/injectable.dart'; - -@prod -@test -@lazySingleton -class DocumentTypeCubit extends LabelCubit { - DocumentTypeCubit(super.metaDataService); - - @override - Future initialize() async { - labelsApi.getDocumentTypes().then(loadFrom); - } - - @override - Future save(DocumentType item) => - labelsApi.saveDocumentType(item); - - @override - Future update(DocumentType item) => - labelsApi.updateDocumentType(item); - - @override - Future delete(DocumentType item) => labelsApi.deleteDocumentType(item); -} diff --git a/lib/features/labels/document_type/view/pages/add_document_type_page.dart b/lib/features/labels/document_type/view/pages/add_document_type_page.dart deleted file mode 100644 index b4aabc5..0000000 --- a/lib/features/labels/document_type/view/pages/add_document_type_page.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; - -class AddDocumentTypePage extends StatelessWidget { - final String? initialName; - const AddDocumentTypePage({Key? key, this.initialName}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AddLabelPage( - addLabelStr: S.of(context).addDocumentTypePageTitle, - fromJson: DocumentType.fromJson, - cubit: BlocProvider.of(context), - initialName: initialName, - ); - } -} diff --git a/lib/features/labels/document_type/view/pages/edit_document_type_page.dart b/lib/features/labels/document_type/view/pages/edit_document_type_page.dart deleted file mode 100644 index 11dbda7..0000000 --- a/lib/features/labels/document_type/view/pages/edit_document_type_page.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart'; -import 'package:paperless_mobile/util.dart'; - -class EditDocumentTypePage extends StatelessWidget { - final DocumentType documentType; - const EditDocumentTypePage({super.key, required this.documentType}); - - @override - Widget build(BuildContext context) { - return EditLabelPage( - label: documentType, - onSubmit: BlocProvider.of(context).replace, - onDelete: (docType) => _onDelete(docType, context), - fromJson: DocumentType.fromJson, - ); - } - - Future _onDelete(DocumentType docType, BuildContext context) async { - try { - await BlocProvider.of(context).remove(docType); - final cubit = BlocProvider.of(context); - if (cubit.state.filter.documentType.id == docType.id) { - cubit.updateFilter( - filter: cubit.state.filter - .copyWith(documentType: const DocumentTypeQuery.unset()), - ); - } - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } finally { - Navigator.pop(context); - } - } -} 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 d9ad55b..b0108ab 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,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/util.dart'; @@ -19,20 +20,26 @@ class DocumentTypeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return AbsorbPointer( - absorbing: !isClickable, - child: GestureDetector( - onTap: () => _addDocumentTypeToFilter(context), - child: BlocBuilder>( - builder: (context, state) { - return Text( - state.labels[documentTypeId]?.toString() ?? "-", - style: Theme.of(context) - .textTheme - .bodyText2! - .copyWith(color: Theme.of(context).colorScheme.tertiary), - ); - }, + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: AbsorbPointer( + absorbing: !isClickable, + child: GestureDetector( + onTap: () => _addDocumentTypeToFilter(context), + child: + BlocBuilder, LabelState>( + builder: (context, state) { + return Text( + state.labels[documentTypeId]?.toString() ?? "-", + style: Theme.of(context) + .textTheme + .bodyText2! + .copyWith(color: Theme.of(context).colorScheme.tertiary), + ); + }, + ), ), ), ); diff --git a/lib/features/labels/storage_path/bloc/storage_path_cubit.dart b/lib/features/labels/storage_path/bloc/storage_path_cubit.dart deleted file mode 100644 index f3f7fb8..0000000 --- a/lib/features/labels/storage_path/bloc/storage_path_cubit.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:injectable/injectable.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; - -@prod -@test -@lazySingleton -class StoragePathCubit extends LabelCubit { - StoragePathCubit(super.metaDataService); - - @override - Future initialize() async { - return labelsApi.getStoragePaths().then(loadFrom); - } - - @override - Future save(StoragePath item) => labelsApi.saveStoragePath(item); - - @override - Future update(StoragePath item) => - labelsApi.updateStoragePath(item); - - @override - Future delete(StoragePath item) => labelsApi.deleteStoragePath(item); -} diff --git a/lib/features/labels/storage_path/view/pages/add_storage_path_page.dart b/lib/features/labels/storage_path/view/pages/add_storage_path_page.dart deleted file mode 100644 index 7dd6227..0000000 --- a/lib/features/labels/storage_path/view/pages/add_storage_path_page.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; -import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; - -class AddStoragePathPage extends StatelessWidget { - final String? initalValue; - const AddStoragePathPage({Key? key, this.initalValue}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AddLabelPage( - addLabelStr: S.of(context).addStoragePathPageTitle, - fromJson: StoragePath.fromJson, - cubit: BlocProvider.of(context), - initialName: initalValue, - additionalFields: const [ - StoragePathAutofillFormBuilderField(name: StoragePath.pathKey), - SizedBox(height: 120.0), - ], - ); - } -} diff --git a/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart b/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart deleted file mode 100644 index 8a4d5b5..0000000 --- a/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; -import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart'; -import 'package:paperless_mobile/util.dart'; - -class EditStoragePathPage extends StatelessWidget { - final StoragePath storagePath; - const EditStoragePathPage({super.key, required this.storagePath}); - - @override - Widget build(BuildContext context) { - return EditLabelPage( - label: storagePath, - onSubmit: BlocProvider.of(context).replace, - onDelete: (correspondent) => _onDelete(correspondent, context), - fromJson: StoragePath.fromJson, - additionalFields: [ - StoragePathAutofillFormBuilderField( - name: StoragePath.pathKey, - initialValue: storagePath.path, - ), - const SizedBox(height: 120.0), - ], - ); - } - - Future _onDelete(StoragePath path, BuildContext context) async { - try { - await BlocProvider.of(context).remove(path); - final cubit = BlocProvider.of(context); - if (cubit.state.filter.storagePath.id == path.id) { - cubit.updateCurrentFilter( - (filter) => filter.copyWith( - storagePath: const StoragePathQuery.unset(), - ), - ); - } - Navigator.pop(context); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } -} 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 bb499d2..ce3fd07 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,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/util.dart'; class StoragePathWidget extends StatelessWidget { @@ -22,22 +23,27 @@ class StoragePathWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return AbsorbPointer( - absorbing: !isClickable, - child: BlocBuilder>( - builder: (context, state) { - return GestureDetector( - onTap: () => _addStoragePathToFilter(context), - child: Text( - state.getLabel(pathId)?.name ?? "-", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2?.copyWith( - color: textColor ?? Theme.of(context).colorScheme.primary, - ), - ), - ); - }, + return BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: AbsorbPointer( + absorbing: !isClickable, + child: BlocBuilder, LabelState>( + builder: (context, state) { + return GestureDetector( + onTap: () => _addStoragePathToFilter(context), + child: Text( + state.getLabel(pathId)?.name ?? "-", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2?.copyWith( + color: textColor ?? Theme.of(context).colorScheme.primary, + ), + ), + ); + }, + ), ), ); } diff --git a/lib/features/labels/tags/bloc/tags_cubit.dart b/lib/features/labels/tags/bloc/tags_cubit.dart deleted file mode 100644 index ae4f247..0000000 --- a/lib/features/labels/tags/bloc/tags_cubit.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -import 'package:injectable/injectable.dart'; - -@prod -@test -@lazySingleton -class TagCubit extends LabelCubit { - TagCubit(super.metaDataService); - - @override - Future initialize() async { - return labelsApi.getTags().then(loadFrom); - } - - @override - Future save(Tag item) => labelsApi.saveTag(item); - - @override - Future update(Tag item) => labelsApi.updateTag(item); - - @override - Future delete(Tag item) => labelsApi.deleteTag(item); -} diff --git a/lib/features/labels/tags/view/pages/add_tag_page.dart b/lib/features/labels/tags/view/pages/add_tag_page.dart deleted file mode 100644 index a714cda..0000000 --- a/lib/features/labels/tags/view/pages/add_tag_page.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; -import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; - -class AddTagPage extends StatelessWidget { - final String? initialValue; - const AddTagPage({Key? key, this.initialValue}) : super(key: key); - - @override - Widget build(BuildContext context) { - return AddLabelPage( - addLabelStr: S.of(context).addTagPageTitle, - fromJson: Tag.fromJson, - cubit: BlocProvider.of(context), - initialName: initialValue, - additionalFields: [ - FormBuilderColorPickerField( - name: Tag.colorKey, - valueTransformer: (color) => "#${color?.value.toRadixString(16)}", - decoration: InputDecoration( - label: Text(S.of(context).tagColorPropertyLabel), - ), - colorPickerType: ColorPickerType.materialPicker, - initialValue: null, - ), - FormBuilderCheckbox( - name: Tag.isInboxTagKey, - title: Text(S.of(context).tagInboxTagPropertyLabel), - ), - ], - ); - } -} diff --git a/lib/features/labels/tags/view/pages/edit_tag_page.dart b/lib/features/labels/tags/view/pages/edit_tag_page.dart deleted file mode 100644 index aba0364..0000000 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; -import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; - -class EditTagPage extends StatelessWidget { - final Tag tag; - - const EditTagPage({super.key, required this.tag}); - - @override - Widget build(BuildContext context) { - return EditLabelPage( - label: tag, - onSubmit: (tag) async { - await BlocProvider.of(context).replace(tag); - }, - onDelete: (tag) => _onDelete(tag, context), - fromJson: Tag.fromJson, - additionalFields: [ - FormBuilderColorPickerField( - initialValue: tag.color, - name: Tag.colorKey, - decoration: InputDecoration( - label: Text(S.of(context).tagColorPropertyLabel), - ), - colorPickerType: ColorPickerType.blockPicker, - ), - FormBuilderCheckbox( - initialValue: tag.isInboxTag, - name: Tag.isInboxTagKey, - title: Text(S.of(context).tagInboxTagPropertyLabel), - ), - ], - ); - } - - Future _onDelete(Tag tag, BuildContext context) async { - try { - await BlocProvider.of(context).remove(tag); - final cubit = BlocProvider.of(context); - final currentFilter = cubit.state.filter; - late DocumentFilter updatedFilter = currentFilter; - if (currentFilter.tags is IdsTagsQuery) { - if ((currentFilter.tags as IdsTagsQuery).includedIds.contains(tag.id)) { - updatedFilter = currentFilter.copyWith( - tags: (currentFilter.tags as IdsTagsQuery).withIdsRemoved( - [tag.id!], - ), - ); - } - } - cubit.updateFilter(filter: updatedFilter); - Navigator.pop(context); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } -} diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index d668af3..dbe7cf1 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -3,9 +3,12 @@ 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:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class TagFormField extends StatefulWidget { @@ -41,11 +44,13 @@ class _TagFormFieldState extends State { @override void initState() { super.initState(); - final state = BlocProvider.of(context).state; _textEditingController = TextEditingController() ..addListener(() { setState(() { - _showCreationSuffixIcon = state.labels.values + _showCreationSuffixIcon = BlocProvider.of>(context) + .state + .labels + .values .where( (item) => item.name.toLowerCase().startsWith( _textEditingController.text.toLowerCase(), @@ -61,117 +66,122 @@ class _TagFormFieldState extends State { @override Widget build(BuildContext context) { - return BlocBuilder>( - builder: (context, tagState) { - return FormBuilderField( - builder: (field) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TypeAheadField( - textFieldConfiguration: TextFieldConfiguration( - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.label_outline, + return TagBlocProvider( + child: BlocBuilder, LabelState>( + builder: (context, tagState) { + return FormBuilderField( + builder: (field) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TypeAheadField( + textFieldConfiguration: TextFieldConfiguration( + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.label_outline, + ), + suffixIcon: _buildSuffixIcon(context, field), + labelText: S.of(context).documentTagsPropertyLabel, + hintText: S.of(context).tagFormFieldSearchHintText, ), - suffixIcon: _buildSuffixIcon(context, field), - labelText: S.of(context).documentTagsPropertyLabel, - hintText: S.of(context).tagFormFieldSearchHintText, + controller: _textEditingController, ), - controller: _textEditingController, - ), - suggestionsCallback: (query) { - final suggestions = tagState.labels.values - .where((element) => element.name - .toLowerCase() - .startsWith(query.toLowerCase())) - .map((e) => e.id!) - .toList(); - if (field.value is IdsTagsQuery) { - suggestions.removeWhere((element) => - (field.value as IdsTagsQuery).ids.contains(element)); - } - if (widget.notAssignedSelectable && - field.value is! OnlyNotAssignedTagsQuery) { - suggestions.insert(0, _onlyNotAssignedId); - } - if (widget.anyAssignedSelectable && - field.value is! AnyAssignedTagsQuery) { - suggestions.insert(0, _anyAssignedId); - } - return suggestions; - }, - getImmediateSuggestions: true, - animationStart: 1, - itemBuilder: (context, data) { - if (data == _onlyNotAssignedId) { + suggestionsCallback: (query) { + final suggestions = tagState.labels.values + .where((element) => element.name + .toLowerCase() + .startsWith(query.toLowerCase())) + .map((e) => e.id!) + .toList(); + if (field.value is IdsTagsQuery) { + suggestions.removeWhere((element) => + (field.value as IdsTagsQuery) + .ids + .contains(element)); + } + if (widget.notAssignedSelectable && + field.value is! OnlyNotAssignedTagsQuery) { + suggestions.insert(0, _onlyNotAssignedId); + } + if (widget.anyAssignedSelectable && + field.value is! AnyAssignedTagsQuery) { + suggestions.insert(0, _anyAssignedId); + } + return suggestions; + }, + getImmediateSuggestions: true, + animationStart: 1, + itemBuilder: (context, data) { + if (data == _onlyNotAssignedId) { + return ListTile( + title: Text(S.of(context).labelNotAssignedText), + ); + } else if (data == _anyAssignedId) { + return ListTile( + title: Text(S.of(context).labelAnyAssignedText), + ); + } + final tag = tagState.getLabel(data)!; return ListTile( - title: Text(S.of(context).labelNotAssignedText), + leading: Icon( + Icons.circle, + color: tag.color, + ), + title: Text( + tag.name, + style: TextStyle( + color: + Theme.of(context).colorScheme.onBackground), + ), ); - } else if (data == _anyAssignedId) { - return ListTile( - title: Text(S.of(context).labelAnyAssignedText), - ); - } - final tag = tagState.getLabel(data)!; - return ListTile( - leading: Icon( - Icons.circle, - color: tag.color, - ), - title: Text( - tag.name, - style: TextStyle( - color: Theme.of(context).colorScheme.onBackground), - ), - ); - }, - onSuggestionSelected: (id) { - if (id == _onlyNotAssignedId) { - //Not assigned tag - field.didChange(const OnlyNotAssignedTagsQuery()); - return; - } else if (id == _anyAssignedId) { - field.didChange(const AnyAssignedTagsQuery()); - } else { - final tagsQuery = field.value is IdsTagsQuery - ? field.value as IdsTagsQuery - : const IdsTagsQuery(); - field.didChange(tagsQuery - .withIdQueriesAdded([IncludeTagIdQuery(id)])); - } - _textEditingController.clear(); - }, - direction: AxisDirection.up, - ), - if (field.value is OnlyNotAssignedTagsQuery) ...[ - _buildNotAssignedTag(field) - ] else if (field.value is AnyAssignedTagsQuery) ...[ - _buildAnyAssignedTag(field) - ] else ...[ - // field.value is IdsTagsQuery - Wrap( - alignment: WrapAlignment.start, - runAlignment: WrapAlignment.start, - spacing: 8.0, - children: ((field.value as IdsTagsQuery).queries) - .map( - (query) => _buildTag( - field, - query, - tagState.getLabel(query.id), - ), - ) - .toList(), + }, + onSuggestionSelected: (id) { + if (id == _onlyNotAssignedId) { + //Not assigned tag + field.didChange(const OnlyNotAssignedTagsQuery()); + return; + } else if (id == _anyAssignedId) { + field.didChange(const AnyAssignedTagsQuery()); + } else { + final tagsQuery = field.value is IdsTagsQuery + ? field.value as IdsTagsQuery + : const IdsTagsQuery(); + field.didChange(tagsQuery + .withIdQueriesAdded([IncludeTagIdQuery(id)])); + } + _textEditingController.clear(); + }, + direction: AxisDirection.up, ), - ] - ], - ); - }, - initialValue: widget.initialValue ?? const IdsTagsQuery(), - name: widget.name, - ); - }, + if (field.value is OnlyNotAssignedTagsQuery) ...[ + _buildNotAssignedTag(field) + ] else if (field.value is AnyAssignedTagsQuery) ...[ + _buildAnyAssignedTag(field) + ] else ...[ + // field.value is IdsTagsQuery + Wrap( + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + spacing: 8.0, + children: ((field.value as IdsTagsQuery).queries) + .map( + (query) => _buildTag( + field, + query, + tagState.getLabel(query.id), + ), + ) + .toList(), + ), + ] + ], + ); + }, + initialValue: widget.initialValue ?? const IdsTagsQuery(), + name: widget.name, + ); + }, + ), ); } @@ -199,8 +209,8 @@ class _TagFormFieldState extends State { void _onAddTag(BuildContext context, FormFieldState field) async { final Tag? tag = await Navigator.of(context).push( MaterialPageRoute( - builder: (_) => BlocProvider.value( - value: BlocProvider.of(context), + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), child: AddTagPage(initialValue: _textEditingController.text), ), ), diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index da091c6..cc7d4bc 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; class TagsWidget extends StatefulWidget { @@ -30,36 +31,38 @@ class TagsWidget extends StatefulWidget { class _TagsWidgetState extends State { @override Widget build(BuildContext context) { - return BlocBuilder>( - builder: (context, state) { - final children = widget.tagIds - .where((id) => state.labels.containsKey(id)) - .map( - (id) => TagWidget( - tag: state.getLabel(id)!, - afterTagTapped: widget.afterTagTapped, - isClickable: widget.isClickable, - isSelected: widget.isSelectedPredicate(id), - onSelected: () => widget.onTagSelected(id), - ), - ) - .toList(); - if (widget.isMultiLine) { - return Wrap( - runAlignment: WrapAlignment.start, - children: children, - runSpacing: 8, - spacing: 4, - ); - } else { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + return TagBlocProvider( + child: BlocBuilder, LabelState>( + builder: (context, state) { + final children = widget.tagIds + .where((id) => state.labels.containsKey(id)) + .map( + (id) => TagWidget( + tag: state.getLabel(id)!, + afterTagTapped: widget.afterTagTapped, + isClickable: widget.isClickable, + isSelected: widget.isSelectedPredicate(id), + onSelected: () => widget.onTagSelected(id), + ), + ) + .toList(); + if (widget.isMultiLine) { + return Wrap( + runAlignment: WrapAlignment.start, children: children, - ), - ); - } - }, + runSpacing: 8, + spacing: 4, + ); + } else { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: children, + ), + ); + } + }, + ), ); } } diff --git a/lib/features/labels/view/pages/add_label_page.dart b/lib/features/labels/view/pages/add_label_page.dart deleted file mode 100644 index 783ba97..0000000 --- a/lib/features/labels/view/pages/add_label_page.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/material.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/type/types.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; - -class AddLabelPage extends StatefulWidget { - final String? initialName; - final String addLabelStr; - final T Function(Map json) fromJson; - final LabelCubit cubit; - final List additionalFields; - - const AddLabelPage({ - Key? key, - this.initialName, - required this.addLabelStr, - required this.fromJson, - required this.cubit, - this.additionalFields = const [], - }) : super(key: key); - - @override - State createState() => _AddLabelPageState(); -} - -class _AddLabelPageState extends State> { - final _formKey = GlobalKey(); - PaperlessValidationErrors _errors = {}; - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: true, - appBar: AppBar( - title: Text(widget.addLabelStr), - ), - 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, - child: ListView( - children: [ - FormBuilderTextField( - autovalidateMode: AutovalidateMode.onUserInteraction, - name: Label.nameKey, - decoration: InputDecoration( - labelText: S.of(context).labelNamePropertyLabel, - errorText: _errors[Label.nameKey], - ), - initialValue: widget.initialName, - validator: FormBuilderValidators.required(), - onChanged: (val) => setState(() => _errors = {}), - ), - FormBuilderTextField( - autovalidateMode: AutovalidateMode.onUserInteraction, - name: Label.matchKey, - decoration: InputDecoration( - labelText: S.of(context).labelMatchPropertyLabel, - ), - onChanged: (val) => setState(() => _errors = {}), - ), - FormBuilderDropdown( - name: Label.matchingAlgorithmKey, - initialValue: MatchingAlgorithm.anyWord.value, - decoration: InputDecoration( - labelText: S.of(context).labelMatchingAlgorithmPropertyLabel, - errorText: _errors[Label.matchingAlgorithmKey], - ), - onChanged: (val) => setState(() => _errors = {}), - items: MatchingAlgorithm.values - .map((algo) => DropdownMenuItem( - child: Text(algo.name), //TODO: INTL - value: algo.value)) - .toList(), - ), - FormBuilderCheckbox( - name: Label.isInsensitiveKey, - initialValue: true, - title: Text(S.of(context).labelIsInsensivitePropertyLabel), - ), - ...widget.additionalFields, - ].padded(), - ), - ), - ); - } - - void _onSubmit() async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - try { - final label = await widget.cubit - .add(widget.fromJson(_formKey.currentState!.value)); - Navigator.pop(context, label); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } on PaperlessValidationErrors catch (json) { - setState(() => _errors = json); - } - } - } -} diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index ace28ab..c3e9ffc 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,22 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; +import 'package:paperless_mobile/core/repository/label_repository.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/edit_label/view/impl/add_tag_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart'; -import 'package:paperless_mobile/features/labels/correspondent/view/pages/edit_correspondent_page.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart'; -import 'package:paperless_mobile/features/labels/document_type/view/pages/edit_document_type_page.dart'; -import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; -import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart'; -import 'package:paperless_mobile/features/labels/storage_path/view/pages/edit_storage_path_page.dart'; -import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; -import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart'; -import 'package:paperless_mobile/features/labels/tags/view/pages/edit_tag_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -35,10 +30,6 @@ class _LabelsPageState extends State @override void initState() { super.initState(); - BlocProvider.of(context).initialize(); - BlocProvider.of(context).initialize(); - BlocProvider.of(context).initialize(); - _tabController = TabController(length: 4, vsync: this) ..addListener(() => setState(() => _currentIndex = _tabController.index)); } @@ -60,7 +51,12 @@ class _LabelsPageState extends State ), actions: [ IconButton( - onPressed: _onAddPressed, + onPressed: [ + _openAddCorrespondentPage, + _openAddDocumentTypePage, + _openAddTagPage, + _openAddStoragePathPage, + ][_currentIndex], icon: const Icon(Icons.add), ) ], @@ -104,69 +100,87 @@ class _LabelsPageState extends State body: TabBarView( controller: _tabController, children: [ - LabelTabView( - cubit: BlocProvider.of(context), - filterBuilder: (label) => DocumentFilter( - correspondent: CorrespondentQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + correspondent: CorrespondentQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: + S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageCorrespondentEmptyStateDescriptionText, + onAddNew: _openAddCorrespondentPage, ), - onOpenEditPage: _openEditCorrespondentPage, - emptyStateActionButtonLabel: - S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel, - emptyStateDescription: S - .of(context) - .labelsPageCorrespondentEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, ), - LabelTabView( - cubit: BlocProvider.of(context), - filterBuilder: (label) => DocumentFilter( - documentType: DocumentTypeQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: DocumentTypeQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: + S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageDocumentTypeEmptyStateDescriptionText, + onAddNew: _openAddDocumentTypePage, ), - onOpenEditPage: _openEditDocumentTypePage, - emptyStateActionButtonLabel: - S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel, - emptyStateDescription: - S.of(context).labelsPageDocumentTypeEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, ), - LabelTabView( - cubit: BlocProvider.of(context), - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), ), - onOpenEditPage: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag ?? false - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, + child: LabelTabView( + filterBuilder: (label) => DocumentFilter( + tags: IdsTagsQuery.fromIds([label.id!]), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + contentBuilder: (t) => Text(t.match ?? ''), + emptyStateActionButtonLabel: + S.of(context).labelsPageTagsEmptyStateAddNewLabel, + emptyStateDescription: + S.of(context).labelsPageTagsEmptyStateDescriptionText, + onAddNew: _openAddTagPage, ), - contentBuilder: (t) => Text(t.match ?? ''), - emptyStateActionButtonLabel: - S.of(context).labelsPageTagsEmptyStateAddNewLabel, - emptyStateDescription: - S.of(context).labelsPageTagsEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, ), - LabelTabView( - cubit: BlocProvider.of(context), - onOpenEditPage: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: StoragePathQuery.fromId(label.id), - pageSize: label.documentCount ?? 0, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), + child: LabelTabView( + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: StoragePathQuery.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => Text(path.path ?? ""), + emptyStateActionButtonLabel: + S.of(context).labelsPageStoragePathEmptyStateAddNewLabel, + emptyStateDescription: S + .of(context) + .labelsPageStoragePathEmptyStateDescriptionText, + onAddNew: _openAddStoragePathPage, ), - contentBuilder: (path) => Text(path.path ?? ""), - emptyStateActionButtonLabel: - S.of(context).labelsPageStoragePathEmptyStateAddNewLabel, - emptyStateDescription: - S.of(context).labelsPageStoragePathEmptyStateDescriptionText, - onOpenAddNewPage: _onAddPressed, ), ], ), @@ -178,12 +192,8 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context), - ), - ], + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), child: EditCorrespondentPage(correspondent: correspondent), ), ), @@ -194,12 +204,8 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context), - ), - ], + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), child: EditDocumentTypePage(documentType: docType), ), ), @@ -210,12 +216,8 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context), - ), - ], + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), child: EditTagPage(tag: tag), ), ), @@ -226,37 +228,61 @@ class _LabelsPageState extends State Navigator.push( context, MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: getIt(), - ), - ], - child: EditStoragePathPage(storagePath: path), + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), + child: EditStoragePathPage( + storagePath: path, + ), ), ), ); } - void _onAddPressed() { - Navigator.push(context, MaterialPageRoute( - builder: (context) { - late final Widget page; - switch (_currentIndex) { - case 0: - page = const AddCorrespondentPage(); - break; - case 1: - page = const AddDocumentTypePage(); - break; - case 2: - page = const AddTagPage(); - break; - case 3: - page = const AddStoragePathPage(); - } - return GlobalStateBlocProvider(child: page); - }, - )); + void _openAddCorrespondentPage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), + child: const AddCorrespondentPage(), + ), + ), + ); + } + + void _openAddDocumentTypePage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), + child: const AddDocumentTypePage(), + ), + ), + ); + } + + void _openAddTagPage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), + child: const AddTagPage(), + ), + ), + ); + } + + void _openAddStoragePathPage() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), + child: const AddStoragePathPage(), + ), + ), + ); } } diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 82edefe..222b0e2 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart'; @@ -46,12 +45,11 @@ class LabelItem extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: getIt() - ..initialize(filter)), - ], + builder: (context) => BlocProvider.value( + value: LinkedDocumentsCubit( + getIt(), + filter, + ), child: const LinkedDocumentsPage(), ), ), diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 299eb14..8821269 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; @@ -9,10 +10,9 @@ import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; class LabelTabView extends StatelessWidget { - final LabelCubit cubit; final DocumentFilter Function(Label) filterBuilder; - final void Function(T) onOpenEditPage; - final void Function() onOpenAddNewPage; + final void Function(T) onEdit; + final void Function() onAddNew; /// Displayed as the subtitle of the [ListTile] final Widget Function(T)? contentBuilder; @@ -26,13 +26,12 @@ class LabelTabView extends StatelessWidget { const LabelTabView({ super.key, - required this.cubit, required this.filterBuilder, this.contentBuilder, this.leadingBuilder, - required this.onOpenEditPage, + required this.onEdit, required this.emptyStateDescription, - required this.onOpenAddNewPage, + required this.onAddNew, required this.emptyStateActionButtonLabel, }); @@ -43,44 +42,40 @@ class LabelTabView extends StatelessWidget { if (state == ConnectivityState.notConnected) { return const OfflineWidget(); } - return RefreshIndicator( - onRefresh: cubit.initialize, - child: BlocBuilder>, LabelState>( - bloc: cubit, - builder: (context, state) { - final labels = state.labels.values.toList()..sort(); - if (labels.isEmpty) { - return Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - emptyStateDescription, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: onOpenAddNewPage, - child: Text(emptyStateActionButtonLabel), - ) - ].padded(), - ), - ); - } - return ListView( - children: labels - .map((l) => LabelItem( - name: l.name, - content: - contentBuilder?.call(l) ?? Text(l.match ?? '-'), - onOpenEditPage: onOpenEditPage, - filterBuilder: filterBuilder, - leading: leadingBuilder?.call(l), - label: l, - )) - .toList(), + return BlocBuilder, LabelState>( + builder: (context, state) { + final labels = state.labels.values.toList()..sort(); + if (labels.isEmpty) { + return Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emptyStateDescription, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: onAddNew, + child: Text(emptyStateActionButtonLabel), + ) + ].padded(), + ), ); - }, - ), + } + return ListView( + children: labels + .map((l) => LabelItem( + name: l.name, + content: + contentBuilder?.call(l) ?? Text(l.match ?? '-'), + onOpenEditPage: onEdit, + filterBuilder: filterBuilder, + leading: leadingBuilder?.call(l), + label: l, + )) + .toList(), + ); + }, ); }, ); diff --git a/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart b/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart index af270de..23064d2 100644 --- a/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents_preview/bloc/linked_documents_cubit.dart @@ -1,24 +1,25 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; -@injectable class LinkedDocumentsCubit extends Cubit { final PaperlessDocumentsApi _api; - LinkedDocumentsCubit(this._api) : super(LinkedDocumentsState()); + LinkedDocumentsCubit(this._api, DocumentFilter filter) + : super(LinkedDocumentsState(filter: filter)) { + _initialize(); + } - Future initialize(DocumentFilter filter) async { + Future _initialize() async { final documents = await _api.find( - filter.copyWith( + state.filter.copyWith( pageSize: 100, ), ); emit(LinkedDocumentsState( isLoaded: true, documents: documents, - filter: filter, + filter: state.filter, )); } } diff --git a/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart b/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart index 80482a9..abb2f4b 100644 --- a/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart +++ b/lib/features/linked_documents_preview/bloc/state/linked_documents_state.dart @@ -3,10 +3,10 @@ import 'package:paperless_api/paperless_api.dart'; class LinkedDocumentsState { final bool isLoaded; final PagedSearchResult? documents; - final DocumentFilter? filter; + final DocumentFilter filter; LinkedDocumentsState({ - this.filter, + required this.filter, this.isLoaded = false, this.documents, }); diff --git a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart index 4fec91f..f7791bb 100644 --- a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart @@ -7,7 +7,6 @@ import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -64,15 +63,12 @@ class _LinkedDocumentsPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (ctxt) => GlobalStateBlocProvider( - additionalProviders: [ + builder: (context) => BlocProvider.value( - value: DocumentDetailsCubit( - getIt(), - document, - ), - ), - ], + value: DocumentDetailsCubit( + getIt(), + document, + ), child: const DocumentDetailsPage( isLabelClickable: false, allowEdit: false, diff --git a/lib/features/saved_view/bloc/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart similarity index 52% rename from lib/features/saved_view/bloc/saved_view_cubit.dart rename to lib/features/saved_view/cubit/saved_view_cubit.dart index d1352d2..3df3e75 100644 --- a/lib/features/saved_view/bloc/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -1,21 +1,27 @@ +import 'dart:async'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; -@prod -@test -@lazySingleton class SavedViewCubit extends Cubit { - final PaperlessSavedViewsApi _api; - SavedViewCubit(this._api) : super(SavedViewState(value: {})); + final SavedViewRepository _repository; + StreamSubscription? _subscription; + + SavedViewCubit(this._repository) : super(SavedViewState(value: {})) { + _subscription = _repository.savedViews.listen( + (savedViews) => emit(state.copyWith(value: savedViews)), + ); + } void selectView(SavedView? view) { emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id)); } Future add(SavedView view) async { - final savedView = await _api.save(view); + final savedView = await _repository.create(view); emit( SavedViewState( value: {...state.value, savedView.id!: savedView}, @@ -26,22 +32,15 @@ class SavedViewCubit extends Cubit { } Future remove(SavedView view) async { - final id = await _api.delete(view); - final newValue = {...state.value}; - newValue.removeWhere((key, value) => key == id); - emit( - SavedViewState( - value: newValue, - selectedSavedViewId: view.id == state.selectedSavedViewId - ? null - : state.selectedSavedViewId, - ), - ); + final id = await _repository.delete(view); + if (state.selectedSavedViewId == id) { + resetSelection(); + } return id; } Future initialize() async { - final views = await _api.getAll(); + final views = await _repository.findAll(); final values = {for (var element in views) element.id!: element}; emit(SavedViewState(value: values)); } @@ -49,4 +48,10 @@ class SavedViewCubit extends Cubit { void resetSelection() { emit(SavedViewState(value: state.value)); } + + @override + Future close() { + _subscription?.cancel(); + return super.close(); + } } diff --git a/lib/features/saved_view/bloc/saved_view_state.dart b/lib/features/saved_view/cubit/saved_view_state.dart similarity index 53% rename from lib/features/saved_view/bloc/saved_view_state.dart rename to lib/features/saved_view/cubit/saved_view_state.dart index d46aeac..682eb88 100644 --- a/lib/features/saved_view/bloc/saved_view_state.dart +++ b/lib/features/saved_view/cubit/saved_view_state.dart @@ -15,4 +15,17 @@ class SavedViewState with EquatableMixin { value, selectedSavedViewId, ]; + + SavedViewState copyWith({ + Map? value, + int? selectedSavedViewId, + bool overwriteSelectedSavedViewId = false, + }) { + return SavedViewState( + value: value ?? this.value, + selectedSavedViewId: overwriteSelectedSavedViewId + ? selectedSavedViewId + : this.selectedSavedViewId, + ); + } } diff --git a/lib/features/saved_view/view/saved_view_selection_widget.dart b/lib/features/saved_view/view/saved_view_selection_widget.dart index 61764b3..18c7eaa 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -5,8 +5,8 @@ import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; -import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; @@ -94,17 +94,10 @@ class SavedViewSelectionWidget extends StatelessWidget { void _onSelected( bool isSelected, BuildContext context, SavedView view) async { - try { - if (isSelected) { - BlocProvider.of(context) - .updateFilter(filter: view.toDocumentFilter()); - BlocProvider.of(context).selectView(view); - } else { - BlocProvider.of(context).updateFilter(); - BlocProvider.of(context).selectView(null); - } - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); + if (isSelected) { + BlocProvider.of(context).selectView(view); + } else { + BlocProvider.of(context).selectView(null); } } diff --git a/lib/features/scan/bloc/document_scanner_cubit.dart b/lib/features/scan/bloc/document_scanner_cubit.dart index 5cdf2a5..1423dcc 100644 --- a/lib/features/scan/bloc/document_scanner_cubit.dart +++ b/lib/features/scan/bloc/document_scanner_cubit.dart @@ -9,13 +9,8 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; -@injectable class DocumentScannerCubit extends Cubit> { - final PaperlessDocumentsApi _api; - - static List initialState = []; - - DocumentScannerCubit(this._api) : super(initialState); + DocumentScannerCubit() : super(const []); void addScan(File file) => emit([...state, file]); @@ -39,41 +34,9 @@ class DocumentScannerCubit extends Cubit> { } } imageCache.clear(); - emit(initialState); + emit([]); } catch (_) { throw const PaperlessServerException(ErrorCode.scanRemoveFailed); } } - - Future uploadDocument( - Uint8List bytes, - String fileName, { - required String title, - required void Function(DocumentModel document)? onConsumptionFinished, - int? documentType, - int? correspondent, - Iterable tags = const [], - DateTime? createdAt, - }) async { - final auth = getIt().state.authentication; - if (auth == null) { - throw const PaperlessServerException(ErrorCode.notAuthenticated); - } - await _api.create( - bytes, - filename: fileName, - title: title, - documentType: documentType, - correspondent: correspondent, - tags: tags, - createdAt: createdAt, - authToken: auth.token!, - serverUrl: auth.serverUrl, - ); - if (onConsumptionFinished != null) { - _api - .waitForConsumptionFinished(fileName, title) - .then((value) => onConsumptionFinished(value)); - } - } } diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart deleted file mode 100644 index 1fd4515..0000000 --- a/lib/features/scan/view/document_upload_page.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/type/types.dart'; -import 'package:paperless_mobile/di_initializer.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; -import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; -import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart'; -import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; -import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; -import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; -import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:paperless_mobile/util.dart'; -import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:intl/date_symbol_data_local.dart'; -import 'package:intl/intl.dart'; - -class DocumentUploadPage extends StatefulWidget { - final Uint8List fileBytes; - final String? title; - final String? filename; - final void Function()? afterUpload; - final void Function(DocumentModel)? onSuccessfullyConsumed; - - const DocumentUploadPage({ - Key? key, - required this.fileBytes, - this.afterUpload, - this.title, - this.filename, - this.onSuccessfullyConsumed, - }) : super(key: key); - - @override - State createState() => _DocumentUploadPageState(); -} - -class _DocumentUploadPageState extends State { - static const fkFileName = "filename"; - static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss"); - - final GlobalKey _formKey = GlobalKey(); - - PaperlessValidationErrors _errors = {}; - bool _isUploadLoading = false; - late bool _syncTitleAndFilename; - final _now = DateTime.now(); - - @override - void initState() { - super.initState(); - _syncTitleAndFilename = widget.filename == null && widget.title == null; - initializeDateFormatting(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: true, - appBar: AppBar( - title: Text(S.of(context).documentsUploadPageTitle), - bottom: _isUploadLoading - ? const PreferredSize( - child: LinearProgressIndicator(), - preferredSize: Size.fromHeight(4.0)) - : null, - ), - 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: FormBuilder( - key: _formKey, - child: ListView( - children: [ - FormBuilderTextField( - autovalidateMode: AutovalidateMode.always, - name: DocumentModel.titleKey, - initialValue: - widget.title ?? "scan_${fileNameDateFormat.format(_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(""); - if (_syncTitleAndFilename) { - _formKey.currentState?.fields[fkFileName]?.didChange(""); - } - }, - ), - errorText: _errors[DocumentModel.titleKey], - ), - onChanged: (value) { - final String transformedValue = _formatFilename(value ?? ''); - if (_syncTitleAndFilename) { - _formKey.currentState?.fields[fkFileName] - ?.didChange(transformedValue); - } - }, - ), - FormBuilderTextField( - autovalidateMode: AutovalidateMode.always, - readOnly: _syncTitleAndFilename, - enabled: !_syncTitleAndFilename, - name: fkFileName, - decoration: InputDecoration( - labelText: S.of(context).documentUploadFileNameLabel, - suffixText: ".pdf", - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => - _formKey.currentState?.fields[fkFileName]?.didChange(''), - ), - ), - initialValue: - widget.filename ?? "scan_${fileNameDateFormat.format(_now)}", - ), - SwitchListTile( - value: _syncTitleAndFilename, - onChanged: (value) { - setState( - () => _syncTitleAndFilename = value, - ); - if (_syncTitleAndFilename) { - final String transformedValue = _formatFilename(_formKey - .currentState - ?.fields[DocumentModel.titleKey] - ?.value as String); - if (_syncTitleAndFilename) { - _formKey.currentState?.fields[fkFileName] - ?.didChange(transformedValue); - } - } - }, - title: Text(S - .of(context) - .documentUploadPageSynchronizeTitleAndFilenameLabel), //TODO: INTL - ), - 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.labels, - 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.labels, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: - CorrespondentQuery.notAssigned, - prefixIcon: const Icon(Icons.person_outline), - ); - }, - ), - const TagFormField( - name: DocumentModel.tagsKey, - notAssignedSelectable: false, - anyAssignedSelectable: false, - excludeAllowed: false, - //Label: "Tags" + " *", - ), - Text( - "* " + S.of(context).uploadPageAutomaticallInferredFieldsHintText, - style: Theme.of(context).textTheme.caption, - ), - ].padded(), - ), - ), - ); - } - - void _onSubmit() async { - if (_formKey.currentState?.saveAndValidate() ?? false) { - final cubit = BlocProvider.of(context); - try { - setState(() => _isUploadLoading = true); - - final fv = _formKey.currentState!.value; - - final createdAt = fv[DocumentModel.createdKey] as DateTime?; - final title = fv[DocumentModel.titleKey] as String; - final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter; - final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery; - final correspondent = - fv[DocumentModel.correspondentKey] as IdQueryParameter; - - await cubit.uploadDocument( - widget.fileBytes, - _padWithPdfExtension(_formKey.currentState?.value[fkFileName]), - onConsumptionFinished: widget.onSuccessfullyConsumed, - title: title, - documentType: docType.id, - correspondent: correspondent.id, - tags: tags.ids, - createdAt: createdAt, - ); - - cubit.reset(); //TODO: Access via provider - showSnackBar(context, S.of(context).documentUploadSuccessText); - Navigator.pop(context); - widget.afterUpload?.call(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } on PaperlessValidationErrors catch (PaperlessServerExceptions) { - setState(() => _errors = PaperlessServerExceptions); - } catch (unknownError, stackTrace) { - showErrorMessage( - context, const PaperlessServerException.unknown(), stackTrace); - } finally { - setState(() { - _isUploadLoading = false; - }); - } - } - } - - String _padWithPdfExtension(String source) { - return source.endsWith(".pdf") ? source : '$source.pdf'; - } - - String _formatFilename(String source) { - return source.replaceAll(RegExp(r"[\W_]"), "_"); - } -} diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index 5df8650..e67ad46 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -9,13 +9,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mime/mime.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/core/global/constants.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; +import 'package:paperless_mobile/core/store/local_vault.dart'; +import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; +import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; -import 'package:paperless_mobile/features/scan/view/document_upload_page.dart'; import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; @@ -124,14 +128,26 @@ class _ScannerPageState extends State final bytes = await doc.save(); Navigator.of(context).push( MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context), + builder: (_) => LabelRepositoriesProvider( + child: BlocProvider( + create: (context) => DocumentUploadCubit( + localVault: getIt(), + documentApi: getIt(), + correspondentRepository: + RepositoryProvider.of>( + context, + ), + documentTypeRepository: + RepositoryProvider.of>( + context, + ), + tagRepository: RepositoryProvider.of>( + context, + ), + ), + child: DocumentUploadPreparationPage( + fileBytes: bytes, ), - ], - child: DocumentUploadPage( - fileBytes: bytes, ), ), ), @@ -243,15 +259,27 @@ class _ScannerPageState extends State } Navigator.of(context).push( MaterialPageRoute( - builder: (_) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value( - value: BlocProvider.of(context), + builder: (_) => LabelRepositoriesProvider( + child: BlocProvider( + create: (context) => DocumentUploadCubit( + localVault: getIt(), + documentApi: getIt(), + correspondentRepository: + RepositoryProvider.of>( + context, + ), + documentTypeRepository: + RepositoryProvider.of>( + context, + ), + tagRepository: RepositoryProvider.of>( + context, + ), + ), + child: DocumentUploadPreparationPage( + fileBytes: fileBytes, + filename: filename, ), - ], - child: DocumentUploadPage( - filename: filename, - fileBytes: fileBytes, ), ), ), diff --git a/lib/main.dart b/lib/main.dart index 0e5f42e..2091935 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,26 +13,31 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; +import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart'; +import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart'; +import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart'; +import 'package:paperless_mobile/core/repository/impl/storage_path_repository_impl.dart'; +import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; +import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; -import 'package:paperless_mobile/features/scan/view/document_upload_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; void main() async { Bloc.observer = BlocChangesObserver(); @@ -52,7 +57,30 @@ void main() async { await getIt().initialize(); await getIt().initialize(); - runApp(const PaperlessMobileEntrypoint()); + // Create repositories + final LabelRepository tagRepository = + TagRepositoryImpl(getIt()); + final LabelRepository correspondentRepository = + CorrespondentRepositoryImpl(getIt()); + final LabelRepository documentTypeRepository = + DocumentTypeRepositoryImpl(getIt()); + final LabelRepository storagePathRepository = + StoragePathRepositoryImpl(getIt()); + final SavedViewRepository savedViewRepository = + SavedViewRepositoryImpl(getIt()); + + runApp( + MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: tagRepository), + RepositoryProvider.value(value: correspondentRepository), + RepositoryProvider.value(value: documentTypeRepository), + RepositoryProvider.value(value: storagePathRepository), + RepositoryProvider.value(value: savedViewRepository), + ], + child: const PaperlessMobileEntrypoint(), + ), + ); } class PaperlessMobileEntrypoint extends StatefulWidget { @@ -71,9 +99,6 @@ class _PaperlessMobileEntrypointState extends State { BlocProvider.value( value: getIt(), ), - BlocProvider.value( - value: getIt(), - ), BlocProvider.value( value: getIt(), ), @@ -126,7 +151,10 @@ class _PaperlessMobileEntrypointState extends State { GlobalWidgetsLocalizations.delegate, FormBuilderLocalizations.delegate, ], - home: const AuthenticationWrapper(), + home: BlocProvider.value( + value: getIt(), + child: const AuthenticationWrapper(), + ), ); }, ), @@ -177,21 +205,21 @@ class _AuthenticationWrapperState extends State { } final filename = extractFilenameFromPath(file.path); final bytes = File(file.path).readAsBytesSync(); - Navigator.push( + final success = await Navigator.push( context, MaterialPageRoute( - builder: (context) => GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value(value: getIt()), - ], - child: DocumentUploadPage( + builder: (context) => BlocProvider.value( + value: getIt(), + child: DocumentUploadPreparationPage( fileBytes: bytes, - afterUpload: SystemNavigator.pop, filename: filename, ), ), ), ); + if (success) { + SystemNavigator.pop(); + } } @override @@ -232,17 +260,12 @@ class _AuthenticationWrapperState extends State { }, builder: (context, authentication) { if (authentication.isAuthenticated) { - return GlobalStateBlocProvider( - additionalProviders: [ - BlocProvider.value(value: getIt()), - ], - child: const HomePage(), - ); + return const HomePage(); } else { - if (authentication.wasLoginStored && - !(authentication.wasLocalAuthenticationSuccessful ?? false)) { - return BiometricAuthenticationPage(); - } + // if (authentication.wasLoginStored && + // !(authentication.wasLocalAuthenticationSuccessful ?? false)) { + // return const BiometricAuthenticationPage(); + // } return const LoginPage(); } }, diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart index d78c8da..b373e55 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart @@ -14,25 +14,25 @@ import 'package:paperless_api/src/models/labels/tag_model.dart'; /// abstract class PaperlessLabelsApi { Future getCorrespondent(int id); - Future> getCorrespondents(); + Future> getCorrespondents([Iterable? ids]); Future saveCorrespondent(Correspondent correspondent); Future updateCorrespondent(Correspondent correspondent); Future deleteCorrespondent(Correspondent correspondent); Future getTag(int id); - Future> getTags({List? ids}); + Future> getTags([Iterable? ids]); Future saveTag(Tag tag); Future updateTag(Tag tag); Future deleteTag(Tag tag); Future getDocumentType(int id); - Future> getDocumentTypes(); + Future> getDocumentTypes([Iterable? ids]); Future saveDocumentType(DocumentType type); Future updateDocumentType(DocumentType documentType); Future deleteDocumentType(DocumentType documentType); Future getStoragePath(int id); - Future> getStoragePaths(); + Future> getStoragePaths([Iterable? ids]); Future saveStoragePath(StoragePath path); Future updateStoragePath(StoragePath path); Future deleteStoragePath(StoragePath path); diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart index f48f4f9..6c4b60b 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart @@ -35,7 +35,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { } @override - Future> getTags({List? ids}) async { + Future> getTags([Iterable? ids]) async { final results = await getCollection( "/api/tags/?page=1&page_size=100000", Tag.fromJson, @@ -59,23 +59,31 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { } @override - Future> getCorrespondents() { - return getCollection( + Future> getCorrespondents([Iterable? ids]) async { + final results = await getCollection( "/api/correspondents/?page=1&page_size=100000", Correspondent.fromJson, ErrorCode.correspondentLoadFailed, client: client, ); + + return results + .where((element) => ids?.contains(element.id) ?? true) + .toList(); } @override - Future> getDocumentTypes() { - return getCollection( + Future> getDocumentTypes([Iterable? ids]) async { + final results = await getCollection( "/api/document_types/?page=1&page_size=100000", DocumentType.fromJson, ErrorCode.documentTypeLoadFailed, client: client, ); + + return results + .where((element) => ids?.contains(element.id) ?? true) + .toList(); } @override @@ -261,13 +269,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { } @override - Future> getStoragePaths() { - return getCollection( + Future> getStoragePaths([Iterable? ids]) async { + final results = await getCollection( "/api/storage_paths/?page=1&page_size=100000", StoragePath.fromJson, ErrorCode.storagePathLoadFailed, client: client, ); + + return results + .where((element) => ids?.contains(element.id) ?? true) + .toList(); } @override diff --git a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api.dart b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api.dart index 60595b2..5b83073 100644 --- a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api.dart +++ b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api.dart @@ -1,7 +1,8 @@ import 'package:paperless_api/src/models/saved_view_model.dart'; abstract class PaperlessSavedViewsApi { - Future> getAll(); + Future find(int id); + Future> findAll([Iterable? ids]); Future save(SavedView view); Future delete(SavedView view); diff --git a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart index 901a998..b014552 100644 --- a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart @@ -14,13 +14,15 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { PaperlessSavedViewsApiImpl(this.client); @override - Future> getAll() { - return getCollection( + Future> findAll([Iterable? ids]) async { + final result = await getCollection( "/api/saved_views/", SavedView.fromJson, ErrorCode.loadSavedViewsError, client: client, ); + + return result.where((view) => ids?.contains(view.id!) ?? true); } @override @@ -51,4 +53,14 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { httpStatusCode: response.statusCode, ); } + + @override + Future find(int id) { + return getSingleResult( + "/api/saved_views/$id/", + SavedView.fromJson, + ErrorCode.loadSavedViewsError, + client: client, + ); + } } diff --git a/pubspec.lock b/pubspec.lock index 186326e..c0564f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1147,12 +1147,12 @@ packages: source: hosted version: "1.4.5" rxdart: - dependency: transitive + dependency: "direct main" description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.4" + version: "0.27.7" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2e5897a..6215f45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,6 +81,7 @@ dependencies: paperless_api: path: packages/paperless_api hive: ^2.2.3 + rxdart: ^0.27.7 dev_dependencies: integration_test: