added initial draft of inbox

This commit is contained in:
Anton Stubenbord
2022-11-22 01:33:50 +01:00
parent a7295fb739
commit 8e7a5dddbf
16 changed files with 243 additions and 27 deletions

View File

@@ -4,12 +4,14 @@ class ErrorMessage implements Exception {
final StackTrace? stackTrace; final StackTrace? stackTrace;
final int? httpStatusCode; final int? httpStatusCode;
const ErrorMessage(this.code, const ErrorMessage(
{this.details, this.stackTrace, this.httpStatusCode}); this.code, {
this.details,
this.stackTrace,
this.httpStatusCode,
});
factory ErrorMessage.unknown() { const ErrorMessage.unknown() : this(ErrorCode.unknown);
return const ErrorMessage(ErrorCode.unknown);
}
@override @override
String toString() { String toString() {

View File

@@ -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'];
}

View File

@@ -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<PaperlessStatistics> getStatistics();
}
@Injectable(as: PaperlessStatisticsService)
class PaperlessStatisticsServiceImpl extends PaperlessStatisticsService {
final BaseClient client;
PaperlessStatisticsServiceImpl(@Named('timeoutClient') this.client);
@override
Future<PaperlessStatistics> 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();
}
}

View File

@@ -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.model.dart';
import 'package:paperless_mobile/features/documents/model/document_filter.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/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:paperless_mobile/features/documents/repository/document_repository.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
@@ -134,6 +135,17 @@ class DocumentsCubit extends Cubit<DocumentsState> {
} }
} }
Future<void> removeInboxTags(
DocumentModel document, final Iterable<int> inboxTags) async {
final updatedTags = document.tags.where((id) => !inboxTags.contains(id));
return updateDocument(
document.copyWith(
tags: updatedTags,
overwriteTags: true,
),
);
}
void resetSelection() { void resetSelection() {
emit(state.copyWith(selection: [])); emit(state.copyWith(selection: []));
} }

View File

@@ -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/document_meta_data.model.dart';
import 'package:paperless_mobile/features/documents/model/paged_search_result.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/documents/model/similar_document.model.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
abstract class DocumentRepository { abstract class DocumentRepository {
Future<void> create( Future<void> create(
@@ -18,7 +19,7 @@ abstract class DocumentRepository {
}); });
Future<DocumentModel> update(DocumentModel doc); Future<DocumentModel> update(DocumentModel doc);
Future<int> findNextAsn(); Future<int> findNextAsn();
Future<PagedSearchResult> find(DocumentFilter filter); Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter);
Future<List<SimilarDocumentModel>> findSimilar(int docId); Future<List<SimilarDocumentModel>> findSimilar(int docId);
Future<int> delete(DocumentModel doc); Future<int> delete(DocumentModel doc);
Future<DocumentMetaData> getMetaData(DocumentModel document); Future<DocumentMetaData> getMetaData(DocumentModel document);

View File

@@ -134,9 +134,10 @@ class DocumentRepositoryImpl implements DocumentRepository {
@override @override
Future<DocumentModel> update(DocumentModel doc) async { Future<DocumentModel> update(DocumentModel doc) async {
final response = await httpClient.put( final response = await httpClient.put(
Uri.parse("/api/documents/${doc.id}/"), Uri.parse("/api/documents/${doc.id}/"),
body: json.encode(doc.toJson()), body: json.encode(doc.toJson()),
headers: {"Content-Type": "application/json"}).timeout(requestTimeout); headers: {"Content-Type": "application/json"},
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return DocumentModel.fromJson( return DocumentModel.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)) as JSON, jsonDecode(utf8.decode(response.bodyBytes)) as JSON,

View File

@@ -7,9 +7,12 @@ import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.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_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_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/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/bottom_navigation_bar.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.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/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_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/storage_path/bloc/storage_path_cubit.dart';
@@ -56,10 +59,12 @@ class _HomePageState extends State<HomePage> {
), ),
drawer: const InfoDrawer(), drawer: const InfoDrawer(),
body: [ body: [
MultiBlocProvider( BlocProvider.value(
providers: [ value: DocumentsCubit(getIt<DocumentRepository>()),
BlocProvider.value(value: getIt<DocumentsCubit>()), child: const InboxPage(),
], ),
BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: const DocumentsPage(), child: const DocumentsPage(),
), ),
BlocProvider.value( BlocProvider.value(

View File

@@ -18,6 +18,14 @@ class BottomNavBar extends StatelessWidget {
onDestinationSelected: onNavigationChanged, onDestinationSelected: onNavigationChanged,
selectedIndex: selectedIndex, selectedIndex: selectedIndex,
destinations: [ destinations: [
NavigationDestination(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: Icon(
Icons.inbox,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context).bottomNavInboxPageLabel,
),
NavigationDestination( NavigationDestination(
icon: const Icon(Icons.description_outlined), icon: const Icon(Icons.description_outlined),
selectedIcon: Icon( selectedIcon: Icon(

View File

@@ -1,8 +1,14 @@
import 'package:badges/badges.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.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/error_message.dart';
import 'package:paperless_mobile/core/model/paperless_server_information.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/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/di_initializer.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/correspondent/bloc/correspondents_cubit.dart';
@@ -123,6 +129,31 @@ class InfoDrawer extends StatelessWidget {
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
), ),
), ),
FutureBuilder<PaperlessStatistics>(
future: getIt<PaperlessStatisticsService>().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<DocumentRepository>()),
child: const InboxPage(),
),
),
)),
);
},
),
Divider(),
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
title: Text( title: Text(

View File

@@ -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<InboxPage> createState() => _InboxPageState();
}
class _InboxPageState extends State<InboxPage> {
Iterable<int> _inboxTags = [];
@override
void initState() {
super.initState();
initializeDateFormatting();
_initInbox();
}
Future<void> _initInbox() async {
final tags = BlocProvider.of<TagCubit>(context).state.values;
_inboxTags = tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!);
final filter = DocumentFilter(tags: IdsTagsQuery.included(_inboxTags));
return BlocProvider.of<DocumentsCubit>(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<DocumentsCubit, DocumentsState>(
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<DocumentsCubit>(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<DocumentsCubit>(context),
child: DocumentDetailsPage(
documentId: doc.id,
allowEdit: false,
isLabelClickable: false,
),
),
),
),
),
),
),
)
.toList(),
)),
],
);
},
),
);
}
}

View File

@@ -27,6 +27,4 @@ abstract class LabelRepository {
Future<StoragePath> saveStoragePath(StoragePath path); Future<StoragePath> saveStoragePath(StoragePath path);
Future<StoragePath> updateStoragePath(StoragePath path); Future<StoragePath> updateStoragePath(StoragePath path);
Future<int> deleteStoragePath(StoragePath path); Future<int> deleteStoragePath(StoragePath path);
Future<int> getStatistics();
} }

View File

@@ -120,15 +120,6 @@ class LabelRepositoryImpl implements LabelRepository {
throw const ErrorMessage(ErrorCode.tagCreateFailed); throw const ErrorMessage(ErrorCode.tagCreateFailed);
} }
@override
Future<int> 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 @override
Future<int> deleteCorrespondent(Correspondent correspondent) async { Future<int> deleteCorrespondent(Correspondent correspondent) async {
assert(correspondent.id != null); assert(correspondent.id != null);

View File

@@ -10,7 +10,13 @@ import 'package:paperless_mobile/util.dart';
class TagWidget extends StatelessWidget { class TagWidget extends StatelessWidget {
final Tag tag; final Tag tag;
final void Function()? afterTagTapped; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -43,6 +49,9 @@ class TagWidget extends StatelessWidget {
} }
void _addTagToFilter(BuildContext context) { void _addTagToFilter(BuildContext context) {
if (!isClickable) {
return;
}
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
try { try {
final tagsQuery = cubit.state.filter.tags is IdsTagsQuery final tagsQuery = cubit.state.filter.tags is IdsTagsQuery

View File

@@ -35,6 +35,7 @@ class _TagsWidgetState extends State<TagsWidget> {
(id) => TagWidget( (id) => TagWidget(
tag: state[id]!, tag: state[id]!,
afterTagTapped: widget.afterTagTapped, afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable,
), ),
) )
.toList(); .toList();

View File

@@ -196,5 +196,6 @@
"labelAnyAssignedText": "Beliebig zugewiesen", "labelAnyAssignedText": "Beliebig zugewiesen",
"deleteViewDialogContentText": "Möchtest Du diese Ansicht wirklich löschen?", "deleteViewDialogContentText": "Möchtest Du diese Ansicht wirklich löschen?",
"deleteViewDialogTitleText": "Lösche Ansicht ", "deleteViewDialogTitleText": "Lösche Ansicht ",
"documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronisiere Titel und Dateiname" "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronisiere Titel und Dateiname",
"bottomNavInboxPageLabel": "Posteingang"
} }

View File

@@ -197,5 +197,6 @@
"labelAnyAssignedText": "Any assigned", "labelAnyAssignedText": "Any assigned",
"deleteViewDialogContentText": "Do you really want to delete this view?", "deleteViewDialogContentText": "Do you really want to delete this view?",
"deleteViewDialogTitleText": "Delete view ", "deleteViewDialogTitleText": "Delete view ",
"documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronize title and filename" "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronize title and filename",
"bottomNavInboxPageLabel": "Inbox"
} }