From 8e7a5dddbf54b529b93c82410f240dc79e67ab38 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 22 Nov 2022 01:33:50 +0100 Subject: [PATCH] added initial draft of inbox --- lib/core/model/error_message.dart | 12 +- lib/core/model/paperless_statistics.dart | 15 +++ .../service/paperless_statistics_service.dart | 29 +++++ .../documents/bloc/documents_cubit.dart | 12 ++ .../repository/document_repository.dart | 3 +- .../repository/document_repository_impl.dart | 7 +- lib/features/home/view/home_page.dart | 13 +- .../view/widget/bottom_navigation_bar.dart | 8 ++ .../home/view/widget/info_drawer.dart | 31 +++++ lib/features/inbox/view/inbox_page.dart | 111 ++++++++++++++++++ .../labels/repository/label_repository.dart | 2 - .../repository/label_repository_impl.dart | 9 -- .../labels/tags/view/widgets/tag_widget.dart | 11 +- .../labels/tags/view/widgets/tags_widget.dart | 1 + lib/l10n/intl_de.arb | 3 +- lib/l10n/intl_en.arb | 3 +- 16 files changed, 243 insertions(+), 27 deletions(-) create mode 100644 lib/core/model/paperless_statistics.dart create mode 100644 lib/core/service/paperless_statistics_service.dart create mode 100644 lib/features/inbox/view/inbox_page.dart diff --git a/lib/core/model/error_message.dart b/lib/core/model/error_message.dart index e49ac4d..03d977d 100644 --- a/lib/core/model/error_message.dart +++ b/lib/core/model/error_message.dart @@ -4,12 +4,14 @@ class ErrorMessage implements Exception { final StackTrace? stackTrace; final int? httpStatusCode; - const ErrorMessage(this.code, - {this.details, this.stackTrace, this.httpStatusCode}); + const ErrorMessage( + this.code, { + this.details, + this.stackTrace, + this.httpStatusCode, + }); - factory ErrorMessage.unknown() { - return const ErrorMessage(ErrorCode.unknown); - } + const ErrorMessage.unknown() : this(ErrorCode.unknown); @override String toString() { diff --git a/lib/core/model/paperless_statistics.dart b/lib/core/model/paperless_statistics.dart new file mode 100644 index 0000000..3e70054 --- /dev/null +++ b/lib/core/model/paperless_statistics.dart @@ -0,0 +1,15 @@ +import 'package:paperless_mobile/core/type/types.dart'; + +class PaperlessStatistics { + final int documentsTotal; + final int documentsInInbox; + + PaperlessStatistics({ + required this.documentsTotal, + required this.documentsInInbox, + }); + + PaperlessStatistics.fromJson(JSON json) + : documentsTotal = json['documents_total'], + documentsInInbox = json['documents_inbox']; +} diff --git a/lib/core/service/paperless_statistics_service.dart b/lib/core/service/paperless_statistics_service.dart new file mode 100644 index 0000000..598e899 --- /dev/null +++ b/lib/core/service/paperless_statistics_service.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/core/model/error_message.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics.dart'; +import 'package:paperless_mobile/core/type/types.dart'; + +abstract class PaperlessStatisticsService { + Future getStatistics(); +} + +@Injectable(as: PaperlessStatisticsService) +class PaperlessStatisticsServiceImpl extends PaperlessStatisticsService { + final BaseClient client; + + PaperlessStatisticsServiceImpl(@Named('timeoutClient') this.client); + + @override + Future getStatistics() async { + final response = await client.get(Uri.parse('/api/statistics/')); + if (response.statusCode == 200) { + return PaperlessStatistics.fromJson( + jsonDecode(utf8.decode(response.bodyBytes)) as JSON, + ); + } + throw const ErrorMessage.unknown(); + } +} diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 28c590c..006451d 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -6,6 +6,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/paged_search_result.dart'; +import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; import 'package:injectable/injectable.dart'; @@ -134,6 +135,17 @@ class DocumentsCubit extends Cubit { } } + Future removeInboxTags( + DocumentModel document, final Iterable inboxTags) async { + final updatedTags = document.tags.where((id) => !inboxTags.contains(id)); + return updateDocument( + document.copyWith( + tags: updatedTags, + overwriteTags: true, + ), + ); + } + void resetSelection() { emit(state.copyWith(selection: [])); } diff --git a/lib/features/documents/repository/document_repository.dart b/lib/features/documents/repository/document_repository.dart index 3acc499..e5c84aa 100644 --- a/lib/features/documents/repository/document_repository.dart +++ b/lib/features/documents/repository/document_repository.dart @@ -5,6 +5,7 @@ import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/document_meta_data.model.dart'; import 'package:paperless_mobile/features/documents/model/paged_search_result.dart'; import 'package:paperless_mobile/features/documents/model/similar_document.model.dart'; +import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; abstract class DocumentRepository { Future create( @@ -18,7 +19,7 @@ abstract class DocumentRepository { }); Future update(DocumentModel doc); Future findNextAsn(); - Future find(DocumentFilter filter); + Future> find(DocumentFilter filter); Future> findSimilar(int docId); Future delete(DocumentModel doc); Future getMetaData(DocumentModel document); diff --git a/lib/features/documents/repository/document_repository_impl.dart b/lib/features/documents/repository/document_repository_impl.dart index 1ef62e2..2c07d18 100644 --- a/lib/features/documents/repository/document_repository_impl.dart +++ b/lib/features/documents/repository/document_repository_impl.dart @@ -134,9 +134,10 @@ class DocumentRepositoryImpl implements DocumentRepository { @override Future update(DocumentModel doc) async { final response = await httpClient.put( - Uri.parse("/api/documents/${doc.id}/"), - body: json.encode(doc.toJson()), - headers: {"Content-Type": "application/json"}).timeout(requestTimeout); + Uri.parse("/api/documents/${doc.id}/"), + body: json.encode(doc.toJson()), + headers: {"Content-Type": "application/json"}, + ); if (response.statusCode == 200) { return DocumentModel.fromJson( jsonDecode(utf8.decode(response.bodyBytes)) as JSON, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 3d4782a..b257425 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -7,9 +7,12 @@ 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/bloc/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository_impl.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/inbox/view/inbox_page.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'; @@ -56,10 +59,12 @@ class _HomePageState extends State { ), drawer: const InfoDrawer(), body: [ - MultiBlocProvider( - providers: [ - BlocProvider.value(value: getIt()), - ], + BlocProvider.value( + value: DocumentsCubit(getIt()), + child: const InboxPage(), + ), + BlocProvider.value( + value: getIt(), child: const DocumentsPage(), ), BlocProvider.value( diff --git a/lib/features/home/view/widget/bottom_navigation_bar.dart b/lib/features/home/view/widget/bottom_navigation_bar.dart index d7ed2a8..24dd4be 100644 --- a/lib/features/home/view/widget/bottom_navigation_bar.dart +++ b/lib/features/home/view/widget/bottom_navigation_bar.dart @@ -18,6 +18,14 @@ class BottomNavBar extends StatelessWidget { onDestinationSelected: onNavigationChanged, selectedIndex: selectedIndex, destinations: [ + NavigationDestination( + icon: const Icon(Icons.inbox_outlined), + selectedIcon: Icon( + Icons.inbox, + color: Theme.of(context).colorScheme.primary, + ), + label: S.of(context).bottomNavInboxPageLabel, + ), NavigationDestination( icon: const Icon(Icons.description_outlined), selectedIcon: Icon( diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index 4965e41..bb10d82 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -1,8 +1,14 @@ +import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/paperless_server_information.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics.dart'; +import 'package:paperless_mobile/core/service/paperless_statistics_service.dart'; +import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; +import 'package:paperless_mobile/features/inbox/view/inbox_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; @@ -123,6 +129,31 @@ class InfoDrawer extends StatelessWidget { color: Theme.of(context).colorScheme.primaryContainer, ), ), + FutureBuilder( + future: getIt().getStatistics(), + builder: (context, snapshot) { + return ListTile( + title: Text("Inbox"), + leading: const Icon(Icons.inbox), + trailing: snapshot.hasData + ? Text( + snapshot.data!.documentsInInbox.toString(), + ) + : null, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LabelBlocProvider( + child: BlocProvider.value( + value: DocumentsCubit(getIt()), + child: const InboxPage(), + ), + ), + )), + ); + }, + ), + Divider(), ListTile( leading: const Icon(Icons.settings), title: Text( diff --git a/lib/features/inbox/view/inbox_page.dart b/lib/features/inbox/view/inbox_page.dart new file mode 100644 index 0000000..4a5ab82 --- /dev/null +++ b/lib/features/inbox/view/inbox_page.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_mobile/core/bloc/label_bloc_provider.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/model/document_filter.dart'; +import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; +import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; +import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; +import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; + +class InboxPage extends StatefulWidget { + const InboxPage({super.key}); + + @override + State createState() => _InboxPageState(); +} + +class _InboxPageState extends State { + Iterable _inboxTags = []; + @override + void initState() { + super.initState(); + initializeDateFormatting(); + _initInbox(); + } + + Future _initInbox() async { + final tags = BlocProvider.of(context).state.values; + _inboxTags = tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!); + final filter = DocumentFilter(tags: IdsTagsQuery.included(_inboxTags)); + return BlocProvider.of(context).updateFilter( + filter: filter, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Inbox"), + ), + drawer: const InfoDrawer(), + floatingActionButton: FloatingActionButton.extended( + label: Text("Mark all as read"), + icon: const Icon(FontAwesomeIcons.checkDouble), + onPressed: () {}, + ), + body: BlocBuilder( + builder: (context, state) { + if (!state.isLoaded) { + return const Center(child: CircularProgressIndicator()); + } + if (state.documents.isEmpty) { + return Text("You do not have new documents in your inbox.") + .padded(); + } + return Column( + children: [ + Text( + "You have ${state.documents.length} documents in your inbox.", + ), + Expanded( + child: ListView( + children: state.documents + .map( + (doc) => Dismissible( + direction: DismissDirection.endToStart, + onDismissed: (_) { + BlocProvider.of(context) + .removeInboxTags(doc, _inboxTags); + }, + key: ObjectKey(doc.id), + child: ListTile( + title: Text(doc.title), + isThreeLine: true, + leading: DocumentPreview(id: doc.id), + subtitle: Text(DateFormat().format(doc.added)), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => LabelBlocProvider( + child: BlocProvider.value( + value: + BlocProvider.of(context), + child: DocumentDetailsPage( + documentId: doc.id, + allowEdit: false, + isLabelClickable: false, + ), + ), + ), + ), + ), + ), + ), + ) + .toList(), + )), + ], + ); + }, + ), + ); + } +} diff --git a/lib/features/labels/repository/label_repository.dart b/lib/features/labels/repository/label_repository.dart index 1c59676..7d117ba 100644 --- a/lib/features/labels/repository/label_repository.dart +++ b/lib/features/labels/repository/label_repository.dart @@ -27,6 +27,4 @@ abstract class LabelRepository { Future saveStoragePath(StoragePath path); Future updateStoragePath(StoragePath path); Future deleteStoragePath(StoragePath path); - - Future getStatistics(); } diff --git a/lib/features/labels/repository/label_repository_impl.dart b/lib/features/labels/repository/label_repository_impl.dart index 8214369..fcbcf28 100644 --- a/lib/features/labels/repository/label_repository_impl.dart +++ b/lib/features/labels/repository/label_repository_impl.dart @@ -120,15 +120,6 @@ class LabelRepositoryImpl implements LabelRepository { throw const ErrorMessage(ErrorCode.tagCreateFailed); } - @override - Future getStatistics() async { - final response = await httpClient.get(Uri.parse('/api/statistics/')); - if (response.statusCode == 200) { - return jsonDecode(utf8.decode(response.bodyBytes))['documents_total']; - } - throw const ErrorMessage(ErrorCode.unknown); - } - @override Future deleteCorrespondent(Correspondent correspondent) async { assert(correspondent.id != null); diff --git a/lib/features/labels/tags/view/widgets/tag_widget.dart b/lib/features/labels/tags/view/widgets/tag_widget.dart index 8c7e98b..f8e4440 100644 --- a/lib/features/labels/tags/view/widgets/tag_widget.dart +++ b/lib/features/labels/tags/view/widgets/tag_widget.dart @@ -10,7 +10,13 @@ import 'package:paperless_mobile/util.dart'; class TagWidget extends StatelessWidget { final Tag tag; final void Function()? afterTagTapped; - const TagWidget({super.key, required this.tag, required this.afterTagTapped}); + final bool isClickable; + const TagWidget({ + super.key, + required this.tag, + required this.afterTagTapped, + this.isClickable = true, + }); @override Widget build(BuildContext context) { @@ -43,6 +49,9 @@ class TagWidget extends StatelessWidget { } void _addTagToFilter(BuildContext context) { + if (!isClickable) { + return; + } final cubit = BlocProvider.of(context); try { final tagsQuery = cubit.state.filter.tags is IdsTagsQuery diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index e95c452..c0b0837 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -35,6 +35,7 @@ class _TagsWidgetState extends State { (id) => TagWidget( tag: state[id]!, afterTagTapped: widget.afterTagTapped, + isClickable: widget.isClickable, ), ) .toList(); diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 9a8ba4b..596b8dc 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -196,5 +196,6 @@ "labelAnyAssignedText": "Beliebig zugewiesen", "deleteViewDialogContentText": "Möchtest Du diese Ansicht wirklich löschen?", "deleteViewDialogTitleText": "Lösche Ansicht ", - "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronisiere Titel und Dateiname" + "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronisiere Titel und Dateiname", + "bottomNavInboxPageLabel": "Posteingang" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index caef897..da133c3 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -197,5 +197,6 @@ "labelAnyAssignedText": "Any assigned", "deleteViewDialogContentText": "Do you really want to delete this view?", "deleteViewDialogTitleText": "Delete view ", - "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronize title and filename" + "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronize title and filename", + "bottomNavInboxPageLabel": "Inbox" } \ No newline at end of file