Implemented inbox (still WIP)

This commit is contained in:
Anton Stubenbord
2022-11-24 13:37:25 +01:00
parent 8e7a5dddbf
commit eb5025e8ca
44 changed files with 674 additions and 316 deletions

View File

@@ -1,55 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> {
final LabelRepository labelRepository;
LabelCubit(this.labelRepository) : super({});
@protected
void loadFrom(Iterable<T> items) =>
emit(Map.fromIterable(items, key: (e) => (e as T).id!));
Future<T> add(T item) async {
assert(item.id == null);
final addedItem = await save(item);
final newState = {...state};
newState.putIfAbsent(addedItem.id!, () => addedItem);
emit(newState);
return addedItem;
}
Future<T> replace(T item) async {
assert(item.id != null);
final updatedItem = await update(item);
final newState = {...state};
newState[item.id!] = updatedItem;
emit(newState);
return updatedItem;
}
Future<void> remove(T item) async {
assert(item.id != null);
if (state.containsKey(item.id)) {
final deletedId = await delete(item);
final newState = {...state};
newState.remove(deletedId);
emit(newState);
}
}
void reset() => emit({});
Future<void> initialize();
@protected
Future<T> save(T item);
@protected
Future<T> update(T item);
@protected
Future<int> delete(T item);
}

View File

@@ -11,7 +11,7 @@ class PaperlessServerInformationCubit
PaperlessServerInformationCubit(this.service)
: super(PaperlessServerInformation());
Future<void> updateStatus() async {
Future<void> updateInformtion() async {
emit(await service.getInformation());
}
}

View File

@@ -0,0 +1,62 @@
import 'dart:math';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/model/paperless_statistics.dart';
import 'package:paperless_mobile/core/model/paperless_statistics_state.dart';
import 'package:paperless_mobile/core/service/paperless_statistics_service.dart';
@singleton
class PaperlessStatisticsCubit extends Cubit<PaperlessStatisticsState> {
final PaperlessStatisticsService statisticsService;
PaperlessStatisticsCubit(this.statisticsService)
: super(PaperlessStatisticsState(isLoaded: false));
Future<void> updateStatistics() async {
final stats = await statisticsService.getStatistics();
emit(PaperlessStatisticsState(isLoaded: true, statistics: stats));
}
void decrementInboxCount() {
if (state.isLoaded) {
emit(
PaperlessStatisticsState(
isLoaded: true,
statistics: PaperlessStatistics(
documentsInInbox: max(0, state.statistics!.documentsInInbox - 1),
documentsTotal: state.statistics!.documentsTotal,
),
),
);
}
}
void incrementInboxCount() {
if (state.isLoaded) {
emit(
PaperlessStatisticsState(
isLoaded: true,
statistics: PaperlessStatistics(
documentsInInbox: state.statistics!.documentsInInbox + 1,
documentsTotal: state.statistics!.documentsTotal,
),
),
);
}
}
void resetInboxCount() {
if (state.isLoaded) {
emit(
PaperlessStatisticsState(
isLoaded: true,
statistics: PaperlessStatistics(
documentsInInbox: 0,
documentsTotal: state.statistics!.documentsTotal,
),
),
);
}
}
}

View File

@@ -38,8 +38,8 @@ String translateError(BuildContext context, ErrorCode code) {
return S.of(context).errorMessageScanRemoveFailed;
case ErrorCode.invalidClientCertificateConfiguration:
return S.of(context).errorMessageInvalidClientCertificateConfiguration;
case ErrorCode.documentBulkDeleteFailed:
return S.of(context).errorMessageBulkDeleteDocumentsFailed;
case ErrorCode.documentBulkActionFailed:
return S.of(context).errorMessageBulkActionFailed;
case ErrorCode.biometricsNotSupported:
return S.of(context).errorMessageBiotmetricsNotSupported;
case ErrorCode.biometricAuthenticationFailed:

View File

@@ -27,7 +27,7 @@ enum ErrorCode {
documentUpdateFailed,
documentLoadFailed,
documentDeleteFailed,
documentBulkDeleteFailed,
documentBulkActionFailed,
documentPreviewFailed,
documentAsnQueryFailed,
tagCreateFailed,

View File

@@ -0,0 +1,11 @@
import 'package:paperless_mobile/core/model/paperless_statistics.dart';
class PaperlessStatisticsState {
final bool isLoaded;
final PaperlessStatistics? statistics;
PaperlessStatisticsState({
required this.isLoaded,
this.statistics,
});
}

View File

@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/bulk_edit.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/paged_search_result.dart';
@@ -36,9 +37,9 @@ class DocumentsCubit extends Cubit<DocumentsState> {
createdAt: createdAt,
);
// documentRepository
// .waitForConsumptionFinished(fileName, title)
// .then((value) => onConsumptionFinished(value));
documentRepository
.waitForConsumptionFinished(fileName, title)
.then((value) => onConsumptionFinished(value));
}
Future<void> removeDocument(DocumentModel document) async {
@@ -47,8 +48,23 @@ class DocumentsCubit extends Cubit<DocumentsState> {
}
Future<void> bulkRemoveDocuments(List<DocumentModel> documents) async {
await documentRepository.bulkDelete(documents);
return await reloadDocuments();
await documentRepository.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)),
);
await reloadDocuments();
}
Future<void> bulkEditTags(
List<DocumentModel> documents, {
Iterable<int> addTags = const [],
Iterable<int> removeTags = const [],
}) async {
await documentRepository.bulkAction(BulkModifyTagsAction(
documents.map((doc) => doc.id),
addTags: addTags,
removeTags: removeTags,
));
await reloadDocuments();
}
Future<void> updateDocument(DocumentModel document) async {
@@ -135,15 +151,20 @@ class DocumentsCubit extends Cubit<DocumentsState> {
}
}
Future<void> removeInboxTags(
///
/// Updates the given document with the inbox tags removed and returns the remoed inbox tags.
///
Future<Iterable<int>> removeInboxTags(
DocumentModel document, final Iterable<int> inboxTags) async {
final updatedTags = document.tags.where((id) => !inboxTags.contains(id));
return updateDocument(
final tagsToRemove = document.tags.toSet().intersection(inboxTags.toSet());
final updatedTags = {...document.tags}..removeAll(tagsToRemove);
await updateDocument(
document.copyWith(
tags: updatedTags,
overwriteTags: true,
),
);
return tagsToRemove;
}
void resetSelection() {

View File

@@ -1,24 +1,50 @@
import 'package:paperless_mobile/core/type/types.dart';
class BulkEditAction {
final List<int> documents;
final BulkEditActionMethod _method;
final Map<String, dynamic> parameters;
abstract class BulkAction {
final Iterable<int> documentIds;
BulkEditAction.delete(this.documents)
: _method = BulkEditActionMethod.delete,
parameters = {};
BulkAction(this.documentIds);
JSON toJson();
}
class BulkDeleteAction extends BulkAction {
BulkDeleteAction(super.documents);
@override
JSON toJson() {
return {
'documents': documents,
'method': _method.name,
'parameters': parameters,
'documents': documentIds.toList(),
'method': 'delete',
};
}
}
enum BulkEditActionMethod {
delete,
edit;
class BulkModifyTagsAction extends BulkAction {
final Iterable<int> removeTags;
final Iterable<int> addTags;
BulkModifyTagsAction(
super.documents, {
this.removeTags = const [],
this.addTags = const [],
});
BulkModifyTagsAction.addTags(super.documents, this.addTags)
: removeTags = const [];
BulkModifyTagsAction.removeTags(super.documents, this.removeTags)
: addTags = const [];
@override
JSON toJson() {
return {
'documents': documentIds.toList(),
'method': 'modify_tags',
'parameters': {
'add_tags': addTags.toList(),
'remove_tags': removeTags.toList(),
}
};
}
}

View File

@@ -17,13 +17,19 @@ class OnlyNotAssignedTagsQuery extends TagsQuery {
}
class AnyAssignedTagsQuery extends TagsQuery {
const AnyAssignedTagsQuery();
final Iterable<int> tagIds;
const AnyAssignedTagsQuery({
this.tagIds = const [],
});
@override
List<Object?> get props => [];
@override
String toQueryParameter() {
return '&is_tagged=1';
if (tagIds.isEmpty) {
return '&is_tagged=1';
}
return '&tags__id__in=${tagIds.join(',')}';
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:paperless_mobile/features/documents/model/bulk_edit.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_meta_data.model.dart';
@@ -23,7 +24,7 @@ abstract class DocumentRepository {
Future<List<SimilarDocumentModel>> findSimilar(int docId);
Future<int> delete(DocumentModel doc);
Future<DocumentMetaData> getMetaData(DocumentModel document);
Future<List<int>> bulkDelete(List<DocumentModel> models);
Future<Iterable<int>> bulkAction(BulkAction action);
Future<Uint8List> getPreview(int docId);
String getThumbnailUrl(int docId);
Future<DocumentModel> waitForConsumptionFinished(

View File

@@ -216,18 +216,16 @@ class DocumentRepositoryImpl implements DocumentRepository {
}
@override
Future<List<int>> bulkDelete(List<DocumentModel> documentModels) async {
final List<int> ids = documentModels.map((e) => e.id).toList();
final action = BulkEditAction.delete(ids);
Future<Iterable<int>> bulkAction(BulkAction action) async {
final response = await httpClient.post(
Uri.parse("/api/documents/bulk_edit/"),
body: json.encode(action.toJson()),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return ids;
return action.documentIds;
} else {
throw const ErrorMessage(ErrorCode.documentBulkDeleteFailed);
throw const ErrorMessage(ErrorCode.documentBulkActionFailed);
}
}

View File

@@ -5,7 +5,8 @@ import 'dart:math';
import 'package:flutter/foundation.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_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
@@ -345,16 +346,20 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return const SizedBox(height: 32.0);
}
void _onEdit(DocumentModel document) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LabelBlocProvider(
child: DocumentEditPage(document: document),
),
maintainState: true,
),
);
void _onEdit(DocumentModel document) async {
final wasUpdated = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => LabelBlocProvider(
child: DocumentEditPage(document: document),
),
maintainState: true,
),
) ??
false;
if (wasUpdated) {
BlocProvider.of<PaperlessStatisticsCubit>(context).updateStatistics();
}
}
Future<void> _onDownload(DocumentModel document) async {

View File

@@ -3,6 +3,8 @@ 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/intl.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -20,6 +22,7 @@ import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_co
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart';
@@ -27,8 +30,6 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie
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';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/intl.dart';
class DocumentEditPage extends StatefulWidget {
final DocumentModel document;
@@ -80,13 +81,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
setState(() {
_isSubmitLoading = true;
});
bool wasUpdated = false;
try {
await getIt<DocumentsCubit>().updateDocument(updatedDocument);
showSnackBar(context, S.of(context).documentUpdateErrorMessage);
wasUpdated = true;
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} finally {
Navigator.pop(context);
Navigator.pop(context, wasUpdated);
}
}
},
@@ -115,7 +118,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
child: ListView(children: [
_buildTitleFormField().padded(),
_buildCreatedAtFormField().padded(),
BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
@@ -130,7 +133,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
label: S.of(context).documentDocumentTypePropertyLabel,
initialValue:
DocumentTypeQuery.fromId(widget.document.documentType),
state: state,
state: state.labels,
name: fkDocumentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder:
@@ -139,7 +142,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
);
},
).padded(),
BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
@@ -150,7 +153,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
child: AddCorrespondentPage(initalValue: initialValue),
),
label: S.of(context).documentCorrespondentPropertyLabel,
state: state,
state: state.labels,
initialValue:
CorrespondentQuery.fromId(widget.document.correspondent),
name: fkCorrespondent,
@@ -161,7 +164,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
);
},
).padded(),
BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>(
notAssignedSelectable: false,
@@ -172,7 +175,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
child: AddStoragePathPage(initalValue: initialValue),
),
label: S.of(context).documentStoragePathPropertyLabel,
state: state,
state: state.labels,
initialValue:
StoragePathQuery.fromId(widget.document.storagePath),
name: fkStoragePath,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
@@ -237,6 +238,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<PaperlessStatisticsCubit>(context)),
],
child: DocumentDetailsPage(
documentId: model.id,

View File

@@ -43,7 +43,6 @@ class DocumentListItem extends StatelessWidget {
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondentId: document.correspondent,
afterSelected: () {},
),
),
],

View File

@@ -18,6 +18,7 @@ import 'package:paperless_mobile/features/labels/correspondent/bloc/corresponden
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
@@ -53,14 +54,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
final _formKey = GlobalKey<FormBuilderState>();
late final DocumentsCubit _documentsCubit;
@override
void initState() {
super.initState();
_documentsCubit = BlocProvider.of<DocumentsCubit>(context);
}
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
if (start == null && end == null) {
return null;
@@ -181,12 +174,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
}
Widget _buildDocumentTypeFormField(DocumentsState docState) {
return BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
return BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
formBuilderState: _formKey.currentState,
name: fkDocumentType,
state: state,
state: state.labels,
label: S.of(context).documentDocumentTypePropertyLabel,
initialValue: docState.filter.documentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
@@ -198,12 +191,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
}
Widget _buildCorrespondentFormField(DocumentsState docState) {
return BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
return BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
formBuilderState: _formKey.currentState,
name: fkCorrespondent,
state: state,
state: state.labels,
label: S.of(context).documentCorrespondentPropertyLabel,
initialValue: docState.filter.correspondent,
queryParameterIdBuilder: CorrespondentQuery.fromId,
@@ -215,12 +208,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
}
Widget _buildStoragePathFormField(DocumentsState docState) {
return BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
return BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>(
formBuilderState: _formKey.currentState,
name: fkStoragePath,
state: state,
state: state.labels,
label: S.of(context).documentStoragePathPropertyLabel,
initialValue: docState.filter.storagePath,
queryParameterIdBuilder: StoragePathQuery.fromId,

View File

@@ -1,18 +1,24 @@
import 'dart:developer';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.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/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.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/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/bloc/label_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/storage_path/bloc/storage_path_cubit.dart';
@@ -20,6 +26,7 @@ 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/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/util.dart';
class HomePage extends StatefulWidget {
@@ -35,7 +42,30 @@ class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
_initializeData(context);
_initializeData(context).then(
(_) async {
FlutterNativeSplash.remove();
if (BlocProvider.of<ApplicationSettingsCubit>(context)
.state
.showInboxOnStartup) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: getIt<PaperlessStatisticsCubit>(),
child: LabelBlocProvider(
child: BlocProvider.value(
value: DocumentsCubit(getIt<DocumentRepository>()),
child: const InboxPage(),
),
),
),
),
);
getIt<DocumentsCubit>().reloadDocuments();
}
},
);
}
@override
@@ -59,10 +89,6 @@ class _HomePageState extends State<HomePage> {
),
drawer: const InfoDrawer(),
body: [
BlocProvider.value(
value: DocumentsCubit(getIt<DocumentRepository>()),
child: const InboxPage(),
),
BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: const DocumentsPage(),
@@ -78,17 +104,21 @@ class _HomePageState extends State<HomePage> {
);
}
_initializeData(BuildContext context) async {
Future<void> _initializeData(BuildContext context) {
try {
await BlocProvider.of<PaperlessServerInformationCubit>(context)
.updateStatus();
BlocProvider.of<DocumentTypeCubit>(context).initialize();
BlocProvider.of<CorrespondentCubit>(context).initialize();
BlocProvider.of<TagCubit>(context).initialize();
BlocProvider.of<StoragePathCubit>(context).initialize();
BlocProvider.of<SavedViewCubit>(context).initialize();
return Future.wait([
BlocProvider.of<PaperlessServerInformationCubit>(context)
.updateInformtion(),
BlocProvider.of<PaperlessStatisticsCubit>(context).updateStatistics(),
BlocProvider.of<DocumentTypeCubit>(context).initialize(),
BlocProvider.of<CorrespondentCubit>(context).initialize(),
BlocProvider.of<TagCubit>(context).initialize(),
BlocProvider.of<StoragePathCubit>(context).initialize(),
BlocProvider.of<SavedViewCubit>(context).initialize(),
]);
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
return Future.error(error);
}
}
}

View File

@@ -18,14 +18,6 @@ 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(

View File

@@ -1,7 +1,9 @@
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_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/paperless_statistics_state.dart';
import 'package:paperless_mobile/features/labels/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';
@@ -57,7 +59,7 @@ class InfoDrawer extends StatelessWidget {
).padded(const EdgeInsets.only(right: 8.0)),
Text(
S.of(context).appTitleText,
style: Theme.of(context).textTheme.headline5!.copyWith(
style: Theme.of(context).textTheme.headline5?.copyWith(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
@@ -104,18 +106,6 @@ class InfoDrawer extends StatelessWidget {
),
],
),
// title: RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text:
// style:
// Theme.of(context).textTheme.bodyText2,
// ),
// ],
// ),
// ),
isThreeLine: true,
),
],
@@ -129,27 +119,32 @@ class InfoDrawer extends StatelessWidget {
color: Theme.of(context).colorScheme.primaryContainer,
),
),
FutureBuilder<PaperlessStatistics>(
future: getIt<PaperlessStatisticsService>().getStatistics(),
builder: (context, snapshot) {
BlocBuilder<PaperlessStatisticsCubit, PaperlessStatisticsState>(
builder: (context, state) {
return ListTile(
title: Text("Inbox"),
title: Text(S.of(context).bottomNavInboxPageLabel),
leading: const Icon(Icons.inbox),
trailing: snapshot.hasData
? Text(
snapshot.data!.documentsInInbox.toString(),
)
trailing: state.isLoaded
? Text(state.statistics!.documentsInInbox.toString())
: null,
onTap: () => Navigator.push(
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LabelBlocProvider(
child: BlocProvider.value(
value: DocumentsCubit(getIt<DocumentRepository>()),
child: const InboxPage(),
builder: (context) => BlocProvider.value(
value: getIt<PaperlessStatisticsCubit>(),
child: LabelBlocProvider(
child: BlocProvider.value(
value:
DocumentsCubit(getIt<DocumentRepository>()),
child: const InboxPage(),
),
),
),
)),
),
);
getIt<DocumentsCubit>().reloadDocuments();
},
);
},
),

View File

@@ -1,18 +1,25 @@
import 'dart:developer';
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/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/model/paperless_statistics_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/documents/model/document.model.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/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class InboxPage extends StatefulWidget {
const InboxPage({super.key});
@@ -22,6 +29,9 @@ class InboxPage extends StatefulWidget {
}
class _InboxPageState extends State<InboxPage> {
static const _a4AspectRatio = 1 / 1.4142;
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
Iterable<int> _inboxTags = [];
@override
void initState() {
@@ -31,9 +41,12 @@ class _InboxPageState extends State<InboxPage> {
}
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));
final tags = BlocProvider.of<TagCubit>(context).state.labels;
log("Loading documents with tags...${tags.values.join(",")}");
_inboxTags =
tags.values.where((t) => t.isInboxTag ?? false).map((t) => t.id!);
final filter =
DocumentFilter(tags: AnyAssignedTagsQuery(tagIds: _inboxTags));
return BlocProvider.of<DocumentsCubit>(context).updateFilter(
filter: filter,
);
@@ -41,71 +54,210 @@ class _InboxPageState extends State<InboxPage> {
@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.",
return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, documentState) {
return Scaffold(
appBar: AppBar(
title:
BlocBuilder<PaperlessStatisticsCubit, PaperlessStatisticsState>(
builder: (context, state) {
return Text(
S.of(context).bottomNavInboxPageLabel +
(state.isLoaded
? ' (${state.statistics!.documentsInInbox})'
: ''),
);
},
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
),
floatingActionButton: documentState.documents.isNotEmpty
? FloatingActionButton.extended(
label: Text("Mark all as seen"),
icon: const Icon(Icons.done_all),
onPressed: () =>
_onMarkAllAsSeen(documentState.documents, _inboxTags),
)
: null,
body: Builder(
builder: (context) {
if (!documentState.isLoaded) {
return const Center(child: CircularProgressIndicator());
}
if (documentState.documents.isEmpty) {
return Text(
"You do not have new documents in your inbox.",
textAlign: TextAlign.center,
) // TODO: INTL
.padded();
}
return Column(
children: [
Text(
'Hint: Swipe left to mark a document as read. This will remove all inbox tags from the document.', //TODO: INTL
style: Theme.of(context).textTheme.caption,
).padded(
const EdgeInsets.only(
top: 4.0,
left: 8.0,
right: 8.0,
bottom: 8.0,
),
),
Expanded(
child: AnimatedList(
key: _listKey,
initialItemCount: documentState.documents.length,
itemBuilder: (context, index, animation) {
final doc = documentState.documents[index];
return _buildListItem(context, doc);
},
),
),
],
);
},
),
);
},
);
}
Widget _buildListItem(BuildContext context, DocumentModel doc) {
return Dismissible(
direction: DismissDirection.endToStart,
background: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
).padded(),
Text(
'Mark as read', //TODO: INTL
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
],
).padded(),
confirmDismiss: (_) => _onItemDismissed(doc),
key: ObjectKey(doc.id),
child: ListTile(
title: Text(doc.title),
isThreeLine: true,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: DocumentPreview(
id: doc.id,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(DateFormat().format(doc.added)),
TagsWidget(tagIds: doc.tags.where((id) => _inboxTags.contains(id)))
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LabelBlocProvider(
child: BlocProvider.value(
value: BlocProvider.of<DocumentsCubit>(context),
child: DocumentDetailsPage(
documentId: doc.id,
allowEdit: false,
isLabelClickable: false,
),
),
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(),
)),
],
);
},
),
),
),
),
);
}
Widget _buildSlideAnimation(
BuildContext context,
animation,
Widget child,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(animation),
child: child,
);
}
Future<void> _onMarkAllAsSeen(
List<DocumentModel> documents,
Iterable<int> inboxTags,
) async {
for (int i = documents.length - 1; i >= 0; i--) {
final doc = documents[i];
_listKey.currentState?.removeItem(
0,
(context, animation) => _buildSlideAnimation(
context,
animation,
_buildListItem(context, doc),
),
);
await Future.delayed(const Duration(milliseconds: 75));
}
await BlocProvider.of<DocumentsCubit>(context)
.bulkEditTags(documents, removeTags: inboxTags);
BlocProvider.of<PaperlessStatisticsCubit>(context).resetInboxCount();
}
Future<bool> _onItemDismissed(DocumentModel doc) async {
try {
final removedTags = await BlocProvider.of<DocumentsCubit>(context)
.removeInboxTags(doc, _inboxTags);
BlocProvider.of<PaperlessStatisticsCubit>(context).decrementInboxCount();
showSnackBar(
context,
'Document removed from inbox.', //TODO: INTL
action: SnackBarAction(
label: 'UNDO', //TODO: INTL
textColor: Theme.of(context).colorScheme.primary,
onPressed: () => _onUndoMarkAsSeen(doc, removedTags),
),
);
return true;
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
return false;
} catch (error) {
showErrorMessage(
context,
const ErrorMessage.unknown(),
);
return false;
}
}
Future<void> _onUndoMarkAsSeen(
DocumentModel doc, Iterable<int> removedTags) async {
try {
await BlocProvider.of<DocumentsCubit>(context).updateDocument(
doc.copyWith(
tags: {...doc.tags, ...removedTags},
overwriteTags: true,
),
);
BlocProvider.of<PaperlessStatisticsCubit>(context).incrementInboxCount();
BlocProvider.of<DocumentsCubit>(context).reloadDocuments();
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
abstract class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final LabelRepository labelRepository;
LabelCubit(this.labelRepository) : super(LabelState.initial());
@protected
void loadFrom(Iterable<T> items) {
emit(
LabelState(
isLoaded: true,
labels: Map.fromIterable(items, key: (e) => (e as T).id!),
),
);
}
Future<T> 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,
),
);
return addedItem;
}
Future<T> 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,
),
);
return updatedItem;
}
Future<void> 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),
);
}
}
void reset() {
emit(LabelState(isLoaded: false, labels: {}));
}
Future<void> initialize();
@protected
Future<T> save(T item);
@protected
Future<T> update(T item);
@protected
Future<int> delete(T item);
}

View File

@@ -1,4 +1,4 @@
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
import 'package:injectable/injectable.dart';

View File

@@ -6,6 +6,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/util.dart';
class CorrespondentWidget extends StatelessWidget {
@@ -16,7 +17,7 @@ class CorrespondentWidget extends StatelessWidget {
const CorrespondentWidget({
Key? key,
required this.correspondentId,
this.correspondentId,
this.afterSelected,
this.textColor,
this.isClickable = true,
@@ -26,12 +27,12 @@ class CorrespondentWidget extends StatelessWidget {
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
child: BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addCorrespondentToFilter(context),
child: Text(
(state[correspondentId]?.name) ?? "-",
(state.getLabel(correspondentId)?.name) ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(

View File

@@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:injectable/injectable.dart';

View File

@@ -5,6 +5,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/util.dart';
class DocumentTypeWidget extends StatelessWidget {
@@ -24,10 +25,10 @@ class DocumentTypeWidget extends StatelessWidget {
absorbing: !isClickable,
child: GestureDetector(
onTap: () => _addDocumentTypeToFilter(context),
child: BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
child: BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
builder: (context, state) {
return Text(
state[documentTypeId]?.toString() ?? "-",
state.labels[documentTypeId]?.toString() ?? "-",
style: Theme.of(context)
.textTheme
.bodyText2!

View File

@@ -0,0 +1,19 @@
import 'package:paperless_mobile/features/labels/model/label.model.dart';
class LabelState<T extends Label> {
LabelState.initial() : this(isLoaded: false, labels: {});
final bool isLoaded;
final Map<int, T> labels;
LabelState({
required this.isLoaded,
required this.labels,
});
T? getLabel(int? key) {
if (isLoaded) {
return labels[key];
}
return null;
}
}

View File

@@ -1,5 +1,5 @@
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
@singleton

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
import 'package:paperless_mobile/util.dart';
@@ -15,7 +16,7 @@ class StoragePathWidget extends StatelessWidget {
const StoragePathWidget({
Key? key,
required this.pathId,
this.pathId,
this.afterSelected,
this.textColor,
this.isClickable = true,
@@ -25,12 +26,12 @@ class StoragePathWidget extends StatelessWidget {
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
child: BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addStoragePathToFilter(context),
child: Text(
(state[pathId]?.name) ?? "-",
state.getLabel(pathId)?.name ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(

View File

@@ -1,4 +1,4 @@
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
import 'package:injectable/injectable.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
@@ -21,7 +22,11 @@ class EditTagPage extends StatelessWidget {
Widget build(BuildContext context) {
return EditLabelPage<Tag>(
label: tag,
onSubmit: BlocProvider.of<TagCubit>(context).replace,
onSubmit: (tag) async {
await BlocProvider.of<TagCubit>(context).replace(tag);
//If inbox property was added/removed from tag, the number of documetns in inbox may increase/decrease.
BlocProvider.of<PaperlessStatisticsCubit>(context).updateStatistics();
},
onDelete: (tag) => _onDelete(tag, context),
fromJson: Tag.fromJson,
additionalFields: [

View File

@@ -3,6 +3,7 @@ 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_mobile/features/documents/model/query_parameters/tags_query.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart';
@@ -45,7 +46,7 @@ class _TagFormFieldState extends State<TagFormField> {
_textEditingController = TextEditingController()
..addListener(() {
setState(() {
_showCreationSuffixIcon = state.values
_showCreationSuffixIcon = state.labels.values
.where(
(item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(),
@@ -61,7 +62,7 @@ class _TagFormFieldState extends State<TagFormField> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TagCubit, Map<int, Tag>>(
return BlocBuilder<TagCubit, LabelState<Tag>>(
builder: (context, tagState) {
return FormBuilderField<TagsQuery>(
builder: (field) {
@@ -81,7 +82,7 @@ class _TagFormFieldState extends State<TagFormField> {
controller: _textEditingController,
),
suggestionsCallback: (query) {
final suggestions = tagState.values
final suggestions = tagState.labels.values
.where((element) => element.name
.toLowerCase()
.startsWith(query.toLowerCase()))
@@ -113,7 +114,7 @@ class _TagFormFieldState extends State<TagFormField> {
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = tagState[data]!;
final tag = tagState.getLabel(data)!;
return ListTile(
leading: Icon(
Icons.circle,
@@ -159,7 +160,7 @@ class _TagFormFieldState extends State<TagFormField> {
(query) => _buildTag(
field,
query,
tagState[query.id]!,
tagState.getLabel(query.id)!,
),
)
.toList(),

View File

@@ -2,6 +2,7 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
@@ -27,13 +28,13 @@ class TagsWidget extends StatefulWidget {
class _TagsWidgetState extends State<TagsWidget> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TagCubit, Map<int, Tag>>(
return BlocBuilder<TagCubit, LabelState<Tag>>(
builder: (context, state) {
final children = widget.tagIds
.where((id) => state.containsKey(id))
.where((id) => state.labels.containsKey(id))
.map(
(id) => TagWidget(
tag: state[id]!,
tag: state.getLabel(id)!,
afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable,
),

View File

@@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/type/types.dart';

View File

@@ -1,6 +1,7 @@
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_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
@@ -227,6 +228,7 @@ class _LabelsPageState extends State<LabelsPage>
providers: [
BlocProvider.value(value: getIt<DocumentsCubit>()),
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)),
BlocProvider.value(value: getIt<PaperlessStatisticsCubit>()),
],
child: EditTagPage(tag: tag),
),

View File

@@ -1,6 +1,6 @@
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/features/labels/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/label_cubit.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/documents/model/document_filter.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/features/labels/model/label_state.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -45,10 +46,10 @@ class LabelTabView<T extends Label> extends StatelessWidget {
}
return RefreshIndicator(
onRefresh: cubit.initialize,
child: BlocBuilder<Cubit<Map<int, T>>, Map<int, T>>(
child: BlocBuilder<Cubit<LabelState<T>>, LabelState<T>>(
bloc: cubit,
builder: (context, state) {
final labels = state.values.toList()..sort();
final labels = state.labels.values.toList()..sort();
if (labels.isEmpty) {
return Center(
child: Column(

View File

@@ -1,6 +1,6 @@
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/features/labels/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.model.dart';

View File

@@ -3,6 +3,7 @@ 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_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart';
@@ -19,6 +20,7 @@ import 'package:paperless_mobile/features/labels/correspondent/model/corresponde
import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
import 'package:paperless_mobile/features/labels/model/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';
@@ -165,7 +167,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
labelText: S.of(context).documentCreatedPropertyLabel + " *",
),
),
BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
bloc: getIt<DocumentTypeCubit>(), //TODO: Use provider
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
@@ -178,7 +180,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
),
label: S.of(context).documentDocumentTypePropertyLabel + " *",
name: DocumentModel.documentTypeKey,
state: state,
state: state.labels,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder:
DocumentTypeQuery.notAssigned,
@@ -186,7 +188,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
);
},
),
BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
bloc: getIt<CorrespondentCubit>(), //TODO: Use provider
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
@@ -200,7 +202,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
label:
S.of(context).documentCorrespondentPropertyLabel + " *",
name: DocumentModel.correspondentKey,
state: state,
state: state.labels,
queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder:
CorrespondentQuery.notAssigned,
@@ -257,7 +259,7 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
} on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = errorMessages);
} catch (unknownError, stackTrace) {
showErrorMessage(context, ErrorMessage.unknown(), stackTrace);
showErrorMessage(context, const ErrorMessage.unknown(), stackTrace);
} finally {
setState(() {
_isUploadLoading = false;
@@ -274,22 +276,23 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
return source.replaceAll(RegExp(r"[\W_]"), "_");
}
void _onConsumptionFinished(document) {
ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar(
SnackBar(
action: SnackBarAction(
onPressed: () async {
try {
getIt<DocumentsCubit>().reloadDocuments();
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
label:
S.of(context).documentUploadProcessingSuccessfulReloadActionText,
),
content: Text(S.of(context).documentUploadProcessingSuccessfulText),
),
);
void _onConsumptionFinished(DocumentModel document) {
// ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar(
// SnackBar(
// action: SnackBarAction(
// onPressed: () async {
// try {
// getIt<DocumentsCubit>().reloadDocuments();
// } on ErrorMessage catch (error, stackTrace) {
// showErrorMessage(context, error, stackTrace);
// }
// },
// label:
// S.of(context).documentUploadProcessingSuccessfulReloadActionText,
// ),
// content: Text(S.of(context).documentUploadProcessingSuccessfulText),
// ),
// );
getIt<PaperlessStatisticsCubit>().incrementInboxCount();
}
}

View File

@@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/service/file_service.dart';

View File

@@ -14,23 +14,27 @@ class ApplicationSettingsState {
preferredLocaleSubtag: Platform.localeName.split('_').first,
preferredThemeMode: ThemeMode.system,
preferredViewType: ViewType.list,
showInboxOnStartup: true,
);
static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled";
static const preferredLocaleSubtagKey = "localeSubtag";
static const preferredThemeModeKey = "preferredThemeModeKey";
static const preferredViewTypeKey = 'preferredViewType';
static const showInboxOnStartupKey = 'showinboxOnStartup';
final bool isLocalAuthenticationEnabled;
final String preferredLocaleSubtag;
final ThemeMode preferredThemeMode;
final ViewType preferredViewType;
final bool showInboxOnStartup;
ApplicationSettingsState({
required this.preferredLocaleSubtag,
required this.preferredThemeMode,
required this.isLocalAuthenticationEnabled,
required this.preferredViewType,
required this.showInboxOnStartup,
});
JSON toJson() {
@@ -43,17 +47,25 @@ class ApplicationSettingsState {
}
ApplicationSettingsState.fromJson(JSON json)
: isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey],
preferredLocaleSubtag = json[preferredLocaleSubtagKey],
preferredThemeMode =
ThemeMode.values.byName(json[preferredThemeModeKey]),
preferredViewType = ViewType.values.byName(json[preferredViewTypeKey]);
: isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey] ??
defaultSettings.isLocalAuthenticationEnabled,
preferredLocaleSubtag = json[preferredLocaleSubtagKey] ??
defaultSettings.preferredLocaleSubtag,
preferredThemeMode = json.containsKey(preferredThemeModeKey)
? ThemeMode.values.byName(json[preferredThemeModeKey])
: defaultSettings.preferredThemeMode,
preferredViewType = json.containsKey(preferredViewTypeKey)
? ViewType.values.byName(json[preferredViewTypeKey])
: defaultSettings.preferredViewType,
showInboxOnStartup =
json[showInboxOnStartupKey] ?? defaultSettings.showInboxOnStartup;
ApplicationSettingsState copyWith({
bool? isLocalAuthenticationEnabled,
String? preferredLocaleSubtag,
ThemeMode? preferredThemeMode,
ViewType? preferredViewType,
bool? showInboxOnStartup,
}) {
return ApplicationSettingsState(
isLocalAuthenticationEnabled:
@@ -62,6 +74,7 @@ class ApplicationSettingsState {
preferredLocaleSubtag ?? this.preferredLocaleSubtag,
preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode,
preferredViewType: preferredViewType ?? this.preferredViewType,
showInboxOnStartup: showInboxOnStartup ?? this.showInboxOnStartup,
);
}
}

View File

@@ -84,7 +84,7 @@
"appSettingsBiometricAuthenticationLabel": "Biometrische Authentifizierung aktivieren",
"appSettingsEnableBiometricAuthenticationReasonText": "Authentifizieren, um die biometrische Authentifizierung zu aktivieren.",
"appSettingsDisableBiometricAuthenticationReasonText": "Authentifizieren, um die biometrische Authentifizierung zu deaktivieren.",
"errorMessageBulkDeleteDocumentsFailed": "Es ist ein Fehler beim massenhaften Löschen der Dokumente aufgetreten.",
"errorMessageBulkActionFailed": "Es ist ein Fehler beim massenhaften bearbeiten der Dokumente aufgetreten.",
"errorMessageBiotmetricsNotSupported": "Biometrische Authentifizierung wird von diesem Gerät nicht unterstützt.",
"errorMessageBiometricAuthenticationFailed": "Biometrische Authentifizierung fehlgeschlagen.",
"errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.",

View File

@@ -83,7 +83,7 @@
"documentPreviewPageTitle": "Preview",
"appSettingsEnableBiometricAuthenticationReasonText": "Authenticate to enable biometric authentication",
"appSettingsDisableBiometricAuthenticationReasonText": "Authenticate to disable biometric authentication",
"errorMessageBulkDeleteDocumentsFailed": "Could not bulk delete documents.",
"errorMessageBulkActionFailed": "Could not bulk edit documents.",
"errorMessageBiotmetricsNotSupported": "Biometric authentication not supported on this device.",
"errorMessageBiometricAuthenticationFailed": "Biometric authentication failed.",
"errorMessageDeviceOffline": "Could not fetch data: You are not connected to the internet.",

View File

@@ -11,7 +11,8 @@ import 'package:intl/intl.dart';
import 'package:intl/intl_standalone.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/global/asset_images.dart';
import 'package:paperless_mobile/core/global/constants.dart';
@@ -194,15 +195,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles);
}
@override
void didChangeDependencies() {
FlutterNativeSplash.remove();
for (var element in AssetImages.values) {
element.load(context);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return SafeArea(
@@ -215,9 +207,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
final bool showIntroSlider =
authState.isAuthenticated && !authState.wasLoginStored;
if (showIntroSlider) {
for (final img in AssetImages.values) {
img.load(context);
}
Navigator.push(
context,
MaterialPageRoute(
@@ -229,10 +218,14 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
},
builder: (context, authentication) {
if (authentication.isAuthenticated) {
return const LabelBlocProvider(
child: HomePage(),
return BlocProvider.value(
value: getIt<PaperlessStatisticsCubit>(),
child: const LabelBlocProvider(
child: HomePage(),
),
);
} else {
FlutterNativeSplash.remove();
return const LoginPage();
}
},