More work on inbox, refactorings (bloc separation of concerns), fixed saved views wrong sort order

This commit is contained in:
Anton Stubenbord
2022-11-29 01:09:36 +01:00
parent 5edbdabf26
commit 50190f035e
43 changed files with 605 additions and 463 deletions

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@@ -1,62 +0,0 @@
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

@@ -4,16 +4,16 @@ enum AssetImages {
headacheDocuments("images/documents_headache.png"), headacheDocuments("images/documents_headache.png"),
organizeDocuments("images/organize_documents.png"), organizeDocuments("images/organize_documents.png"),
secureDocuments("images/secure_documents.png"), secureDocuments("images/secure_documents.png"),
success("images/success.png"); success("images/success.png"),
emptyInbox("images/empty_inbox.png");
final String relativePath; final String relativePath;
const AssetImages(String relativePath) const AssetImages(String relativePath)
: relativePath = "assets/$relativePath"; : relativePath = "assets/$relativePath";
Image get image => Image.asset( AssetImage get image => AssetImage(relativePath);
relativePath,
key: ObjectKey("assetimage_$relativePath"),
);
void load(context) => precacheImage(image.image, context); void load(context) => precacheImage(image, context);
} }
late Image emptyInboxImage;

View File

@@ -15,7 +15,7 @@ class AuthenticationInterceptor implements InterceptorContract {
Future<BaseRequest> interceptRequest({required BaseRequest request}) async { Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
final authState = authenticationCubit.state; final authState = authenticationCubit.state;
if (kDebugMode) { if (kDebugMode) {
log("Intercepted request to ${request.url.toString()}"); log("Intercepted ${request.method} request to ${request.url.toString()}");
} }
if (authState.authentication == null) { if (authState.authentication == null) {
throw const ErrorMessage(ErrorCode.notAuthenticated); throw const ErrorMessage(ErrorCode.notAuthenticated);

View File

@@ -18,9 +18,8 @@ class ApplicationIntroSlideshow extends StatefulWidget {
} }
class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> { class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
Image organizeImage = AssetImages.organizeDocuments.image; AssetImage secureImage = AssetImages.secureDocuments.image;
Image secureImage = AssetImages.secureDocuments.image; AssetImage successImage = AssetImages.success.image;
Image successImage = AssetImages.success.image;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -50,7 +49,9 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
), ),
image: Padding( image: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: organizeImage, child: Image(
image: AssetImages.organizeDocuments.image,
),
), ),
bodyWidget: Column( bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -70,7 +71,7 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
), ),
image: Padding( image: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: secureImage, child: Image(image: AssetImages.secureDocuments.image),
), ),
bodyWidget: Column( bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -90,7 +91,7 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
), ),
image: Padding( image: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: successImage, child: Image(image: AssetImages.success.image),
), ),
bodyWidget: Column( bodyWidget: Column(
children: const [ children: const [

View File

@@ -0,0 +1,31 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/features/documents/model/document.model.dart';
import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
part 'document_details_state.dart';
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final DocumentRepository _documentRepository;
DocumentDetailsCubit(this._documentRepository, DocumentModel initialDocument)
: super(DocumentDetailsState(document: initialDocument));
Future<void> delete(DocumentModel document) async {
await _documentRepository.delete(document);
emit(const DocumentDetailsState());
}
Future<void> update(DocumentModel document) async {
final updatedDocument = await _documentRepository.update(document);
emit(DocumentDetailsState(document: updatedDocument));
}
Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) {
final int asn = await _documentRepository.findNextAsn();
update(document.copyWith(archiveSerialNumber: asn));
}
}
}

View File

@@ -0,0 +1,12 @@
part of 'document_details_cubit.dart';
class DocumentDetailsState with EquatableMixin {
final DocumentModel? document;
const DocumentDetailsState({
this.document,
});
@override
List<Object?> get props => [document];
}

View File

@@ -5,15 +5,13 @@ import 'dart:math';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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/paperless_statistics_cubit.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:intl/intl.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/model/error_message.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_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.model.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/repository/document_repository.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
@@ -21,26 +19,26 @@ import 'package:paperless_mobile/features/documents/view/pages/document_edit_pag
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class DocumentDetailsPage extends StatefulWidget { class DocumentDetailsPage extends StatefulWidget {
final int documentId;
final bool allowEdit; final bool allowEdit;
final bool isLabelClickable; final bool isLabelClickable;
final String? titleAndContentQueryString;
const DocumentDetailsPage({ const DocumentDetailsPage({
Key? key, Key? key,
required this.documentId,
this.allowEdit = true,
this.isLabelClickable = true, this.isLabelClickable = true,
this.titleAndContentQueryString,
this.allowEdit = true,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -48,130 +46,194 @@ class DocumentDetailsPage extends StatefulWidget {
} }
class _DocumentDetailsPageState extends State<DocumentDetailsPage> { class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
static final DateFormat _detailedDateFormat = @override
DateFormat("MMM d, yyyy HH:mm:ss"); void initState() {
super.initState();
initializeDateFormatting();
}
bool _isDownloadPending = false; bool _isDownloadPending = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DocumentsCubit, DocumentsState>( return WillPopScope(
// buildWhen required because rebuild would happen after delete causing error. onWillPop: () {
buildWhen: (previous, current) { print("Returning document...");
return current.documents Navigator.of(context)
.where((element) => element.id == widget.documentId) .pop(BlocProvider.of<DocumentDetailsCubit>(context).state.document);
.isNotEmpty; return Future.value(false);
}, },
builder: (context, state) { child: DefaultTabController(
final document = length: 3,
state.documents.where((doc) => doc.id == widget.documentId).first; child: Scaffold(
return DefaultTabController( floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
length: 3, floatingActionButton: widget.allowEdit
child: Scaffold( ? FloatingActionButton(
floatingActionButtonLocation: child: const Icon(Icons.edit),
FloatingActionButtonLocation.endDocked, onPressed: _onEdit,
floatingActionButton: widget.allowEdit )
? FloatingActionButton( : null,
child: const Icon(Icons.edit), bottomNavigationBar:
onPressed: () => _onEdit(document), BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
) builder: (context, state) {
: null, return BottomAppBar(
bottomNavigationBar: BottomAppBar( child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
children: [ IconButton(
IconButton( icon: const Icon(Icons.delete),
icon: const Icon(Icons.delete), onPressed: widget.allowEdit && state.document != null
onPressed: ? () => _onDelete(state.document!)
widget.allowEdit ? () => _onDelete(document) : null, : null,
).padded(const EdgeInsets.symmetric(horizontal: 4)), ).padded(const EdgeInsets.symmetric(horizontal: 4)),
IconButton( IconButton(
icon: const Icon(Icons.download), icon: const Icon(Icons.download),
onPressed: onPressed: Platform.isAndroid && state.document != null
Platform.isAndroid ? () => _onDownload(document) : null, ? () => _onDownload(state.document!)
).padded(const EdgeInsets.only(right: 4)), : null,
IconButton( ).padded(const EdgeInsets.only(right: 4)),
icon: const Icon(Icons.open_in_new), IconButton(
onPressed: () => _onOpen(document), icon: const Icon(Icons.open_in_new),
).padded(const EdgeInsets.only(right: 4)), onPressed: state.document != null
IconButton( ? () => _onOpen(state.document!)
icon: const Icon(Icons.share), : null,
onPressed: () => _onShare(document), ).padded(const EdgeInsets.only(right: 4)),
), IconButton(
], icon: const Icon(Icons.share),
), onPressed: state.document != null
), ? () => _onShare(state.document!)
body: NestedScrollView( : null,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors
.black, //TODO: check if there is a way to dynamically determine color...
), ),
onPressed: () => Navigator.pop(context), ],
),
);
},
),
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors
.black, //TODO: check if there is a way to dynamically determine color...
), ),
floating: true, onPressed: () => Navigator.pop(
pinned: true, context,
expandedHeight: 200.0, BlocProvider.of<DocumentDetailsCubit>(context)
flexibleSpace: DocumentPreview( .state
id: document.id, .document),
fit: BoxFit.cover, ),
), floating: true,
bottom: ColoredTabBar( pinned: true,
backgroundColor: expandedHeight: 200.0,
Theme.of(context).colorScheme.primaryContainer, flexibleSpace:
tabBar: TabBar( BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
tabs: [ builder: (context, state) {
Tab( if (state.document == null) {
child: Text( return Container(height: 200);
S.of(context).documentDetailsPageTabOverviewLabel, }
style: TextStyle( return DocumentPreview(
color: Theme.of(context) id: state.document!.id,
.colorScheme fit: BoxFit.cover,
.onPrimaryContainer), );
), },
),
bottom: ColoredTabBar(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
tabBar: TabBar(
tabs: [
Tab(
child: Text(
S.of(context).documentDetailsPageTabOverviewLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
), ),
Tab( ),
child: Text( Tab(
S.of(context).documentDetailsPageTabContentLabel, child: Text(
style: TextStyle( S.of(context).documentDetailsPageTabContentLabel,
color: Theme.of(context) style: TextStyle(
.colorScheme color: Theme.of(context)
.onPrimaryContainer), .colorScheme
), .onPrimaryContainer),
), ),
Tab( ),
child: Text( Tab(
S.of(context).documentDetailsPageTabMetaDataLabel, child: Text(
style: TextStyle( S.of(context).documentDetailsPageTabMetaDataLabel,
color: Theme.of(context) style: TextStyle(
.colorScheme color: Theme.of(context)
.onPrimaryContainer), .colorScheme
), .onPrimaryContainer),
), ),
], ),
), ],
), ),
), ),
],
body: TabBarView(
children: [
_buildDocumentOverview(
document, state.filter.titleAndContentMatchString),
_buildDocumentContentView(
document, state.filter.titleAndContentMatchString),
_buildDocumentMetaDataView(document),
].padded(),
), ),
],
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
if (state.document == null) {
return TabBarView(
children: [
Container(),
Container(),
Container(),
],
);
}
return TabBarView(
children: [
_buildDocumentOverview(
state.document!,
widget.titleAndContentQueryString,
),
_buildDocumentContentView(
state.document!,
widget.titleAndContentQueryString,
),
_buildDocumentMetaDataView(
state.document!,
),
].padded(),
);
},
), ),
), ),
); ),
}, ),
); );
} }
Future<void> _onEdit() async {
{
final cubit = BlocProvider.of<DocumentDetailsCubit>(context);
if (cubit.state.document == null) {
return;
}
Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
child: DocumentEditPage(
document: cubit.state.document!,
onEdit: (updatedDocument) {
return BlocProvider.of<DocumentDetailsCubit>(context)
.update(updatedDocument);
},
),
),
maintainState: false,
),
);
}
}
Widget _buildDocumentMetaDataView(DocumentModel document) { Widget _buildDocumentMetaDataView(DocumentModel document) {
return FutureBuilder<DocumentMetaData>( return FutureBuilder<DocumentMetaData>(
future: getIt<DocumentRepository>().getMetaData(document), future: getIt<DocumentRepository>().getMetaData(document),
@@ -182,11 +244,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final meta = snapshot.data!; final meta = snapshot.data!;
return ListView( return ListView(
children: [ children: [
_DetailsItem.text(_detailedDateFormat.format(document.modified), _DetailsItem.text(DateFormat().format(document.modified),
label: S.of(context).documentModifiedPropertyLabel, label: S.of(context).documentModifiedPropertyLabel,
context: context), context: context),
_separator(), _separator(),
_DetailsItem.text(_detailedDateFormat.format(document.added), _DetailsItem.text(DateFormat().format(document.added),
label: S.of(context).documentAddedPropertyLabel, label: S.of(context).documentAddedPropertyLabel,
context: context), context: context),
_separator(), _separator(),
@@ -233,7 +295,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Future<void> _assignAsn(DocumentModel document) async { Future<void> _assignAsn(DocumentModel document) async {
try { try {
await BlocProvider.of<DocumentsCubit>(context).assignAsn(document); await BlocProvider.of<DocumentDetailsCubit>(context).assignAsn(document);
} on ErrorMessage catch (error, stackTrace) { } on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
@@ -265,8 +327,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
_separator(), _separator(),
_DetailsItem.text( _DetailsItem.text(
DateFormat.yMMMd(Localizations.localeOf(context).toLanguageTag()) DateFormat().format(document.created),
.format(document.created),
context: context, context: context,
label: S.of(context).documentCreatedPropertyLabel, label: S.of(context).documentCreatedPropertyLabel,
), ),
@@ -311,6 +372,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: TagsWidget( child: TagsWidget(
isClickable: widget.isLabelClickable, isClickable: widget.isLabelClickable,
tagIds: document.tags, tagIds: document.tags,
isSelectedPredicate: (_) => false,
onTagSelected: (int tagId) {},
), ),
), ),
), ),
@@ -345,22 +408,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return const SizedBox(height: 32.0); return const SizedBox(height: 32.0);
} }
void _onEdit(DocumentModel document) async {
final wasUpdated = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
child: DocumentEditPage(document: document),
),
maintainState: true,
),
) ??
false;
if (wasUpdated) {
BlocProvider.of<PaperlessStatisticsCubit>(context).updateStatistics();
}
}
Future<void> _onDownload(DocumentModel document) async { Future<void> _onDownload(DocumentModel document) async {
if (!Platform.isAndroid) { if (!Platform.isAndroid) {
showSnackBar( showSnackBar(
@@ -411,12 +458,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
false; false;
if (delete) { if (delete) {
try { try {
await BlocProvider.of<DocumentsCubit>(context).remove(document); await BlocProvider.of<DocumentDetailsCubit>(context).delete(document);
showSnackBar(context, S.of(context).documentDeleteSuccessMessage); showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
} on ErrorMessage catch (error, stackTrace) { } on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} finally { } finally {
Navigator.pop(context); // Document deleted => go back to primary route
Navigator.popUntil(context, (route) => route.isFirst);
} }
} }
} }

View File

@@ -15,11 +15,6 @@ class DocumentsCubit extends Cubit<DocumentsState> {
DocumentsCubit(this.documentRepository) : super(DocumentsState.initial); DocumentsCubit(this.documentRepository) : super(DocumentsState.initial);
Future<void> remove(DocumentModel document) async {
await documentRepository.delete(document);
await reload();
}
Future<void> bulkRemove(List<DocumentModel> documents) async { Future<void> bulkRemove(List<DocumentModel> documents) async {
await documentRepository.bulkAction( await documentRepository.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)), BulkDeleteAction(documents.map((doc) => doc.id)),
@@ -40,8 +35,13 @@ class DocumentsCubit extends Cubit<DocumentsState> {
await reload(); await reload();
} }
Future<void> update(DocumentModel document) async { Future<void> update(
await documentRepository.update(document); DocumentModel document, [
bool updateRemote = true,
]) async {
if (updateRemote) {
await documentRepository.update(document);
}
await reload(); await reload();
} }
@@ -83,13 +83,6 @@ class DocumentsCubit extends Cubit<DocumentsState> {
isLoaded: true, value: [...state.value, result], filter: newFilter)); isLoaded: true, value: [...state.value, result], filter: newFilter));
} }
Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) {
final int asn = await documentRepository.findNextAsn();
update(document.copyWith(archiveSerialNumber: asn));
}
}
/// ///
/// Update filter state and automatically reload documents. Always resets page to 1. /// Update filter state and automatically reload documents. Always resets page to 1.
/// Use [DocumentsCubit.loadMore] to load more data. /// Use [DocumentsCubit.loadMore] to load more data.

View File

@@ -16,6 +16,7 @@ class BulkDeleteAction extends BulkAction {
return { return {
'documents': documentIds.toList(), 'documents': documentIds.toList(),
'method': 'delete', 'method': 'delete',
'parameters': {},
}; };
} }
} }
@@ -33,8 +34,9 @@ class BulkModifyTagsAction extends BulkAction {
BulkModifyTagsAction.addTags(super.documents, this.addTags) BulkModifyTagsAction.addTags(super.documents, this.addTags)
: removeTags = const []; : removeTags = const [];
BulkModifyTagsAction.removeTags(super.documents, this.removeTags) BulkModifyTagsAction.removeTags(super.documents, Iterable<int> tags)
: addTags = const []; : addTags = const [],
removeTags = tags;
@override @override
JSON toJson() { JSON toJson() {

View File

@@ -2,8 +2,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
class DocumentModel extends Equatable { class DocumentModel extends Equatable {
static const idKey = 'id'; static const idKey = 'id';

View File

@@ -61,7 +61,7 @@ class SavedView with EquatableMixin {
DocumentFilter toDocumentFilter() { DocumentFilter toDocumentFilter() {
return filterRules.fold( return filterRules.fold(
DocumentFilter( DocumentFilter(
sortOrder: sortReverse ? SortOrder.ascending : SortOrder.descending, sortOrder: sortReverse ? SortOrder.descending : SortOrder.ascending,
sortField: sortField, sortField: sortField,
), ),
(filter, filterRule) => filterRule.applyToFilter(filter), (filter, filterRule) => filterRule.applyToFilter(filter),
@@ -80,7 +80,7 @@ class SavedView with EquatableMixin {
sortField: filter.sortField, sortField: filter.sortField,
showInSidebar: showInSidebar, showInSidebar: showInSidebar,
showOnDashboard: showOnDashboard, showOnDashboard: showOnDashboard,
sortReverse: filter.sortOrder == SortOrder.ascending, sortReverse: filter.sortOrder == SortOrder.descending,
); );
JSON toJson() { JSON toJson() {

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -33,7 +34,13 @@ import 'package:paperless_mobile/util.dart';
class DocumentEditPage extends StatefulWidget { class DocumentEditPage extends StatefulWidget {
final DocumentModel document; final DocumentModel document;
const DocumentEditPage({Key? key, required this.document}) : super(key: key); final FutureOr<void> Function(DocumentModel updatedDocument) onEdit;
const DocumentEditPage({
Key? key,
required this.document,
required this.onEdit,
}) : super(key: key);
@override @override
State<DocumentEditPage> createState() => _DocumentEditPageState(); State<DocumentEditPage> createState() => _DocumentEditPageState();
@@ -66,7 +73,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
onPressed: () async { onPressed: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value; final values = _formKey.currentState!.value;
final updatedDocument = widget.document.copyWith( var updatedDocument = widget.document.copyWith(
title: values[fkTitle], title: values[fkTitle],
created: values[fkCreatedDate], created: values[fkCreatedDate],
overwriteDocumentType: true, overwriteDocumentType: true,
@@ -81,15 +88,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
setState(() { setState(() {
_isSubmitLoading = true; _isSubmitLoading = true;
}); });
bool wasUpdated = false;
try { try {
await getIt<DocumentsCubit>().update(updatedDocument); await widget.onEdit(updatedDocument);
showSnackBar(context, S.of(context).documentUpdateErrorMessage); showSnackBar(context, S.of(context).documentUpdateSuccessMessage);
wasUpdated = true;
} on ErrorMessage catch (error, stackTrace) { } on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} finally { } finally {
Navigator.pop(context, wasUpdated); setState(() {
_isSubmitLoading = false;
});
Navigator.pop(context);
} }
} }
}, },

View File

@@ -2,12 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.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/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/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.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.model.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_details_page.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/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart';
@@ -160,22 +163,25 @@ class _DocumentsPageState extends State<DocumentsPage> {
switch (settings.preferredViewType) { switch (settings.preferredViewType) {
case ViewType.list: case ViewType.list:
child = DocumentListView( child = DocumentListView(
onTap: _openDocumentDetails, onTap: _openDetails,
state: state, state: state,
onSelected: _onSelected, onSelected: _onSelected,
pagingController: _pagingController, pagingController: _pagingController,
hasInternetConnection: hasInternetConnection:
connectivityState == ConnectivityState.connected, connectivityState == ConnectivityState.connected,
onTagSelected: _addTagToFilter,
); );
break; break;
case ViewType.grid: case ViewType.grid:
child = DocumentGridView( child = DocumentGridView(
onTap: _openDocumentDetails, onTap: _openDetails,
state: state, state: state,
onSelected: _onSelected, onSelected: _onSelected,
pagingController: _pagingController, pagingController: _pagingController,
hasInternetConnection: hasInternetConnection:
connectivityState == ConnectivityState.connected); connectivityState == ConnectivityState.connected,
onTagSelected: (int tagId) => _addTagToFilter,
);
break; break;
} }
@@ -222,26 +228,63 @@ class _DocumentsPageState extends State<DocumentsPage> {
); );
} }
void _openDocumentDetails(DocumentModel model) { Future<void> _openDetails(DocumentModel document) async {
Navigator.push( await Navigator.of(context).push<DocumentModel?>(
context, _buildDetailsPageRoute(document),
MaterialPageRoute( );
builder: (_) => MultiBlocProvider( BlocProvider.of<DocumentsCubit>(context).reload();
providers: [ }
BlocProvider.value(value: BlocProvider.of<DocumentsCubit>(context)),
BlocProvider.value( MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
value: BlocProvider.of<CorrespondentCubit>(context)), DocumentModel document) {
BlocProvider.value( return MaterialPageRoute(
value: BlocProvider.of<DocumentTypeCubit>(context)), builder: (_) => MultiBlocProvider(
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)), providers: [
BlocProvider.value( BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(context)), value: BlocProvider.of<DocumentsCubit>(context),
BlocProvider.value( ),
value: BlocProvider.of<PaperlessStatisticsCubit>(context)), BlocProvider.value(
], value: BlocProvider.of<CorrespondentCubit>(context),
child: DocumentDetailsPage(documentId: model.id), ),
), BlocProvider.value(
value: BlocProvider.of<DocumentTypeCubit>(context),
),
BlocProvider.value(
value: BlocProvider.of<TagCubit>(context),
),
BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(context),
),
BlocProvider.value(
value: DocumentDetailsCubit(getIt<DocumentRepository>(), document),
),
],
child: const DocumentDetailsPage(),
), ),
); );
} }
void _addTagToFilter(int tagId) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
final tagsQuery = cubit.state.filter.tags is IdsTagsQuery
? cubit.state.filter.tags as IdsTagsQuery
: const IdsTagsQuery();
if (tagsQuery.includedIds.contains(tagId)) {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
tags: tagsQuery.withIdsRemoved([tagId]),
),
);
} else {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tagId)]),
),
);
}
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
} }

View File

@@ -4,8 +4,8 @@ import 'package:paperless_mobile/core/widgets/empty_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.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/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -23,7 +23,7 @@ class DocumentsEmptyState extends StatelessWidget {
title: S.of(context).documentsPageEmptyStateOopsText, title: S.of(context).documentsPageEmptyStateOopsText,
subtitle: S.of(context).documentsPageEmptyStateNothingHereText, subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
bottomChild: state.filter != DocumentFilter.initial bottomChild: state.filter != DocumentFilter.initial
? ElevatedButton( ? TextButton(
onPressed: () async { onPressed: () async {
await BlocProvider.of<DocumentsCubit>(context).updateFilter(); await BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).resetSelection(); BlocProvider.of<SavedViewCubit>(context).resetSelection();

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_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/query_parameters/tags_query.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
@@ -11,6 +12,7 @@ class DocumentGridView extends StatelessWidget {
final PagingController<int, DocumentModel> pagingController; final PagingController<int, DocumentModel> pagingController;
final DocumentsState state; final DocumentsState state;
final bool hasInternetConnection; final bool hasInternetConnection;
final void Function(int tagId) onTagSelected;
const DocumentGridView({ const DocumentGridView({
super.key, super.key,
@@ -19,6 +21,7 @@ class DocumentGridView extends StatelessWidget {
required this.state, required this.state,
required this.onSelected, required this.onSelected,
required this.hasInternetConnection, required this.hasInternetConnection,
required this.onTagSelected,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -38,6 +41,14 @@ class DocumentGridView extends StatelessWidget {
isSelected: state.selection.contains(item), isSelected: state.selection.contains(item),
onSelected: onSelected, onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty, isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
); );
}, },
noItemsFoundIndicatorBuilder: (context) => noItemsFoundIndicatorBuilder: (context) =>

View File

@@ -12,6 +12,8 @@ class DocumentGridItem extends StatelessWidget {
final void Function(DocumentModel) onTap; final void Function(DocumentModel) onTap;
final void Function(DocumentModel) onSelected; final void Function(DocumentModel) onSelected;
final bool isAtLeastOneSelected; final bool isAtLeastOneSelected;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId) onTagSelected;
const DocumentGridItem({ const DocumentGridItem({
Key? key, Key? key,
@@ -20,6 +22,8 @@ class DocumentGridItem extends StatelessWidget {
required this.onSelected, required this.onSelected,
required this.isSelected, required this.isSelected,
required this.isAtLeastOneSelected, required this.isAtLeastOneSelected,
required this.isTagSelectedPredicate,
required this.onTagSelected,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -65,6 +69,8 @@ class DocumentGridItem extends StatelessWidget {
TagsWidget( TagsWidget(
tagIds: document.tags, tagIds: document.tags,
isMultiLine: false, isMultiLine: false,
isSelectedPredicate: isTagSelectedPredicate,
onTagSelected: onTagSelected,
), ),
const Spacer(), const Spacer(),
Text( Text(

View File

@@ -3,8 +3,10 @@ import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart
import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_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/query_parameters/tags_query.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
class DocumentListView extends StatelessWidget { class DocumentListView extends StatelessWidget {
final void Function(DocumentModel) onTap; final void Function(DocumentModel) onTap;
@@ -14,6 +16,8 @@ class DocumentListView extends StatelessWidget {
final DocumentsState state; final DocumentsState state;
final bool hasInternetConnection; final bool hasInternetConnection;
final bool isLabelClickable; final bool isLabelClickable;
final void Function(int tagId) onTagSelected;
const DocumentListView({ const DocumentListView({
super.key, super.key,
required this.onTap, required this.onTap,
@@ -22,6 +26,7 @@ class DocumentListView extends StatelessWidget {
required this.onSelected, required this.onSelected,
required this.hasInternetConnection, required this.hasInternetConnection,
this.isLabelClickable = true, this.isLabelClickable = true,
required this.onTagSelected,
}); });
@override @override
@@ -38,6 +43,14 @@ class DocumentListView extends StatelessWidget {
isSelected: state.selection.contains(document), isSelected: state.selection.contains(document),
onSelected: onSelected, onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty, isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
); );
}, },
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection noItemsFoundIndicatorBuilder: (context) => hasInternetConnection

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.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/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
class DocumentListItem extends StatelessWidget { class DocumentListItem extends StatelessWidget {
@@ -12,6 +13,9 @@ class DocumentListItem extends StatelessWidget {
final bool isSelected; final bool isSelected;
final bool isAtLeastOneSelected; final bool isAtLeastOneSelected;
final bool isLabelClickable; final bool isLabelClickable;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId) onTagSelected;
const DocumentListItem({ const DocumentListItem({
Key? key, Key? key,
@@ -21,6 +25,8 @@ class DocumentListItem extends StatelessWidget {
required this.isSelected, required this.isSelected,
required this.isAtLeastOneSelected, required this.isAtLeastOneSelected,
this.isLabelClickable = true, this.isLabelClickable = true,
required this.isTagSelectedPredicate,
required this.onTagSelected,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -62,6 +68,8 @@ class DocumentListItem extends StatelessWidget {
isClickable: isLabelClickable, isClickable: isLabelClickable,
tagIds: document.tags, tagIds: document.tags,
isMultiLine: false, isMultiLine: false,
isSelectedPredicate: isTagSelectedPredicate,
onTagSelected: onTagSelected,
), ),
), ),
), ),

View File

@@ -5,7 +5,6 @@ import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.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/query_parameters/correspondent_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
@@ -23,6 +22,7 @@ import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.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'; 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/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';

View File

@@ -1,10 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/documents/model/saved_view.model.dart'; import 'package:paperless_mobile/features/documents/model/saved_view.model.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class ConfirmDeleteSavedViewDialog extends StatelessWidget { class ConfirmDeleteSavedViewDialog extends StatelessWidget {
const ConfirmDeleteSavedViewDialog({ const ConfirmDeleteSavedViewDialog({

View File

@@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/error_message.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_state.dart';
import 'package:paperless_mobile/features/documents/model/saved_view.model.dart'; import 'package:paperless_mobile/features/documents/model/saved_view.model.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/add_saved_view_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/add_saved_view_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';

View File

@@ -3,12 +3,10 @@ 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/connectivity_cubit.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_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/model/error_message.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart'; 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/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';
@@ -20,6 +18,7 @@ import 'package:paperless_mobile/features/labels/document_type/bloc/document_typ
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';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -88,7 +87,6 @@ class _HomePageState extends State<HomePage> {
return Future.wait([ return Future.wait([
BlocProvider.of<PaperlessServerInformationCubit>(context) BlocProvider.of<PaperlessServerInformationCubit>(context)
.updateInformtion(), .updateInformtion(),
getIt<PaperlessStatisticsCubit>().updateStatistics(),
getIt<DocumentTypeCubit>().initialize(), getIt<DocumentTypeCubit>().initialize(),
getIt<CorrespondentCubit>().initialize(), getIt<CorrespondentCubit>().initialize(),
getIt<TagCubit>().initialize(), getIt<TagCubit>().initialize(),

View File

@@ -1,7 +1,6 @@
import 'package:badges/badges.dart'; 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/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/paperless_statistics_state.dart'; import 'package:paperless_mobile/core/model/paperless_statistics_state.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
@@ -120,17 +119,10 @@ class InfoDrawer extends StatelessWidget {
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
), ),
), ),
BlocBuilder<PaperlessStatisticsCubit, PaperlessStatisticsState>( ListTile(
builder: (context, state) { title: Text(S.of(context).bottomNavInboxPageLabel),
return ListTile( leading: const Icon(Icons.inbox),
title: Text(S.of(context).bottomNavInboxPageLabel), onTap: () => _onOpenInbox(context),
leading: const Icon(Icons.inbox),
trailing: state.isLoaded
? Text(state.statistics!.documentsInInbox.toString())
: null,
onTap: () => _onOpenInbox(context),
);
},
), ),
Divider(), Divider(),
ListTile( ListTile(
@@ -217,15 +209,9 @@ class InfoDrawer extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider( builder: (_) => GlobalStateBlocProvider(
additionalProviders: [ additionalProviders: [
BlocProvider<PaperlessStatisticsCubit>.value(
value: BlocProvider.of<PaperlessStatisticsCubit>(context),
),
BlocProvider<InboxCubit>.value( BlocProvider<InboxCubit>.value(
value: getIt<InboxCubit>()..initialize(), value: getIt<InboxCubit>()..initialize(),
), ),
BlocProvider<DocumentsCubit>.value(
value: getIt<DocumentsCubit>(),
),
], ],
child: const InboxPage(), child: const InboxPage(),
), ),

View File

@@ -48,11 +48,13 @@ class InboxCubit extends Cubit<InboxState> {
sortField: SortField.added, sortField: SortField.added,
)) ))
.then((psr) => psr.results); .then((psr) => psr.results);
emit(InboxState( emit(
isLoaded: true, InboxState(
inboxItems: inboxDocuments, isLoaded: true,
inboxTags: state.inboxTags, inboxItems: inboxDocuments,
)); inboxTags: state.inboxTags,
),
);
} }
/// ///
@@ -61,12 +63,13 @@ class InboxCubit extends Cubit<InboxState> {
/// ///
Future<Iterable<int>> remove(DocumentModel document) async { Future<Iterable<int>> remove(DocumentModel document) async {
if (!state.isLoaded) { if (!state.isLoaded) {
throw "State has not yet loaded. Ensure the state is loaded when calling this method!"; throw "State has not loaded yet. Ensure the state is loaded when calling this method!";
} }
final tagsToRemove = final tagsToRemove =
document.tags.toSet().intersection(state.inboxTags.toSet()); document.tags.toSet().intersection(state.inboxTags.toSet());
final updatedTags = {...document.tags}..removeAll(tagsToRemove); final updatedTags = {...document.tags}..removeAll(tagsToRemove);
await _documentRepository.update( await _documentRepository.update(
document.copyWith( document.copyWith(
tags: updatedTags, tags: updatedTags,
@@ -84,31 +87,43 @@ class InboxCubit extends Cubit<InboxState> {
return tagsToRemove; return tagsToRemove;
} }
///
/// Adds the previously removed tags to the document and performs an update.
///
Future<void> undoRemove( Future<void> undoRemove(
DocumentModel document, Iterable<int> removedTags) async { DocumentModel document,
Iterable<int> removedTags,
) async {
final updatedDoc = document.copyWith( final updatedDoc = document.copyWith(
tags: {...document.tags, ...removedTags}, tags: {...document.tags, ...removedTags},
overwriteTags: true, overwriteTags: true,
); );
await _documentRepository.update(updatedDoc); await _documentRepository.update(updatedDoc);
emit(InboxState( emit(
isLoaded: true, InboxState(
inboxItems: [...state.inboxItems, updatedDoc] isLoaded: true,
..sort((d1, d2) => d1.added.compareTo(d2.added)), inboxItems: [...state.inboxItems, updatedDoc]
inboxTags: state.inboxTags, ..sort((d1, d2) => d2.added.compareTo(d1.added)),
)); inboxTags: state.inboxTags,
),
);
} }
/// ///
/// Removes inbox tags from all documents in the inbox. /// Removes inbox tags from all documents in the inbox.
/// ///
Future<void> clearInbox() async { Future<void> clearInbox() async {
await _documentRepository.bulkAction(BulkModifyTagsAction.removeTags( await _documentRepository.bulkAction(
state.inboxItems.map((e) => e.id), state.inboxTags)); BulkModifyTagsAction.removeTags(
state.inboxItems.map((e) => e.id),
state.inboxTags,
),
);
emit( emit(
InboxState( InboxState(
isLoaded: true, isLoaded: true,
inboxTags: state.inboxTags, inboxTags: state.inboxTags,
inboxItems: [],
), ),
); );
} }

View File

@@ -1,11 +1,9 @@
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:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.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/error_message.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
@@ -21,7 +19,8 @@ class InboxPage extends StatefulWidget {
} }
class _InboxPageState extends State<InboxPage> { class _InboxPageState extends State<InboxPage> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>(); final GlobalKey<RefreshIndicatorState> _emptyStateRefreshIndicatorKey =
GlobalKey();
@override @override
void initState() { void initState() {
@@ -31,7 +30,7 @@ class _InboxPageState extends State<InboxPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bloc = BlocProvider.of<InboxCubit>(context); //TODO: Group by date (today, yseterday, etc.)
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(S.of(context).bottomNavInboxPageLabel), title: Text(S.of(context).bottomNavInboxPageLabel),
@@ -39,6 +38,27 @@ class _InboxPageState extends State<InboxPage> {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
bottom: PreferredSize(
preferredSize: Size.fromHeight(14),
child: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerRight,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: ColoredBox(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Text(
'${state.inboxItems.length} unseen',
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.caption,
).padded(const EdgeInsets.symmetric(horizontal: 4.0)),
),
),
);
},
).padded(const EdgeInsets.symmetric(horizontal: 8.0)),
),
), ),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>( floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) { builder: (context, state) {
@@ -47,8 +67,8 @@ class _InboxPageState extends State<InboxPage> {
icon: const Icon(Icons.done_all), icon: const Icon(Icons.done_all),
onPressed: state.isLoaded && state.inboxItems.isNotEmpty onPressed: state.isLoaded && state.inboxItems.isNotEmpty
? () => _onMarkAllAsSeen( ? () => _onMarkAllAsSeen(
bloc.state.inboxItems, state.inboxItems,
bloc.state.inboxTags, state.inboxTags,
) )
: null, : null,
); );
@@ -61,10 +81,26 @@ class _InboxPageState extends State<InboxPage> {
} }
if (state.inboxItems.isEmpty) { if (state.inboxItems.isEmpty) {
return Text( return RefreshIndicator(
"You do not have new documents in your inbox.", key: _emptyStateRefreshIndicatorKey,
textAlign: TextAlign.center, onRefresh: () =>
).padded(); BlocProvider.of<InboxCubit>(context).reloadInbox(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You do not have unseen documents.'),
TextButton(
onPressed: () =>
_emptyStateRefreshIndicatorKey.currentState?.show(),
child: Text('Refresh'),
),
],
),
),
);
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => BlocProvider.of<InboxCubit>(context).reloadInbox(), onRefresh: () => BlocProvider.of<InboxCubit>(context).reloadInbox(),
@@ -84,10 +120,9 @@ class _InboxPageState extends State<InboxPage> {
), ),
), ),
Expanded( Expanded(
child: AnimatedList( child: ListView.builder(
key: _listKey, itemCount: state.inboxItems.length,
initialItemCount: state.inboxItems.length, itemBuilder: (context, index) {
itemBuilder: (context, index, animation) {
final doc = state.inboxItems.elementAt(index); final doc = state.inboxItems.elementAt(index);
return _buildListItem(context, doc); return _buildListItem(context, doc);
}, },
@@ -108,11 +143,11 @@ class _InboxPageState extends State<InboxPage> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Icon( Icon(
Icons.done, Icons.done_all,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
).padded(), ).padded(),
Text( Text(
'Mark as read', //TODO: INTL 'Mark as seen', //TODO: INTL
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
@@ -120,57 +155,49 @@ class _InboxPageState extends State<InboxPage> {
], ],
).padded(), ).padded(),
confirmDismiss: (_) => _onItemDismissed(doc), confirmDismiss: (_) => _onItemDismissed(doc),
key: ObjectKey(doc.id), key: UniqueKey(),
child: DocumentInboxItem(document: doc), child: DocumentInboxItem(document: doc),
); );
} }
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( Future<void> _onMarkAllAsSeen(
Iterable<DocumentModel> documents, Iterable<DocumentModel> documents,
Iterable<int> inboxTags, Iterable<int> inboxTags,
) async { ) async {
for (int i = documents.length - 1; i >= 0; i--) { final isActionConfirmed = await showDialog(
final doc = documents.elementAt(i); context: context,
_listKey.currentState?.removeItem( builder: (context) => AlertDialog(
0, title: Text('Confirm action'),
(context, animation) => _buildSlideAnimation( content: Text(
context, 'Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents.\nThis action is not reversible! Are you sure you want to continue?',
animation, ),
_buildListItem(context, doc), actions: [
), TextButton(
); onPressed: () => Navigator.of(context).pop(false),
await Future.delayed(const Duration(milliseconds: 75)); child: Text(S.of(context).genericActionCancelLabel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(S.of(context).genericActionOkLabel),
),
],
),
) ??
false;
if (isActionConfirmed) {
await BlocProvider.of<InboxCubit>(context).clearInbox();
} }
await BlocProvider.of<DocumentsCubit>(context)
.bulkEditTags(documents, removeTags: inboxTags);
BlocProvider.of<PaperlessStatisticsCubit>(context).resetInboxCount();
} }
Future<bool> _onItemDismissed(DocumentModel doc) async { Future<bool> _onItemDismissed(DocumentModel doc) async {
try { try {
final removedTags = final removedTags =
await BlocProvider.of<InboxCubit>(context).remove(doc); await BlocProvider.of<InboxCubit>(context).remove(doc);
BlocProvider.of<PaperlessStatisticsCubit>(context).decrementInboxCount();
showSnackBar( showSnackBar(
context, context,
'Document removed from inbox.', //TODO: INTL 'Document removed from inbox.', //TODO: INTL
action: SnackBarAction( action: SnackBarAction(
label: 'UNDO', //TODO: INTL label: 'UNDO', //TODO: INTL
textColor: Theme.of(context).colorScheme.primary,
onPressed: () => _onUndoMarkAsSeen(doc, removedTags), onPressed: () => _onUndoMarkAsSeen(doc, removedTags),
), ),
); );
@@ -194,7 +221,6 @@ class _InboxPageState extends State<InboxPage> {
try { try {
await BlocProvider.of<InboxCubit>(context) await BlocProvider.of<InboxCubit>(context)
.undoRemove(document, removedTags); .undoRemove(document, removedTags);
BlocProvider.of<PaperlessStatisticsCubit>(context).incrementInboxCount();
} on ErrorMessage catch (error, stackTrace) { } on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }

View File

@@ -1,9 +1,12 @@
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:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.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/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
@@ -37,6 +40,8 @@ class DocumentInboxItem extends StatelessWidget {
tagIds: document.tags, tagIds: document.tags,
isMultiLine: false, isMultiLine: false,
isClickable: false, isClickable: false,
isSelectedPredicate: (_) => false,
onTagSelected: (_) {},
), ),
], ],
), ),
@@ -45,11 +50,14 @@ class DocumentInboxItem extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider( builder: (_) => GlobalStateBlocProvider(
additionalProviders: [ additionalProviders: [
BlocProvider.value( BlocProvider<DocumentDetailsCubit>(
value: BlocProvider.of<DocumentsCubit>(context)), create: (context) => DocumentDetailsCubit(
getIt<DocumentRepository>(),
document,
),
),
], ],
child: DocumentDetailsPage( child: const DocumentDetailsPage(
documentId: document.id,
allowEdit: false, allowEdit: false,
isLabelClickable: false, isLabelClickable: false,
), ),

View File

@@ -1,11 +1,11 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.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';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
class GlobalStateBlocProvider extends StatelessWidget { class GlobalStateBlocProvider extends StatelessWidget {
final List<BlocProvider> additionalProviders; final List<BlocProvider> additionalProviders;

View File

@@ -1,7 +1,6 @@
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:flutter_form_builder/flutter_form_builder.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/model/error_message.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/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart';
@@ -24,8 +23,6 @@ class EditTagPage extends StatelessWidget {
label: tag, label: tag,
onSubmit: (tag) async { onSubmit: (tag) async {
await BlocProvider.of<TagCubit>(context).replace(tag); await BlocProvider.of<TagCubit>(context).replace(tag);
//If inbox property was added/removed from tag, the number of documents in inbox may increase/decrease.
BlocProvider.of<PaperlessStatisticsCubit>(context).updateStatistics();
}, },
onDelete: (tag) => _onDelete(tag, context), onDelete: (tag) => _onDelete(tag, context),
fromJson: Tag.fromJson, fromJson: Tag.fromJson,

View File

@@ -1,80 +1,43 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
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/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
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 VoidCallback? afterTagTapped;
final VoidCallback onSelected;
final bool isSelected;
final bool isClickable; final bool isClickable;
const TagWidget({ const TagWidget({
super.key, super.key,
required this.tag, required this.tag,
required this.afterTagTapped, required this.afterTagTapped,
this.isClickable = true, this.isClickable = true,
required this.onSelected,
required this.isSelected,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: BlocBuilder<DocumentsCubit, DocumentsState>( child: AbsorbPointer(
builder: (context, state) { absorbing: !isClickable,
final isIdsQuery = state.filter.tags is IdsTagsQuery; child: FilterChip(
return FilterChip( selected: isSelected,
selected: isIdsQuery selectedColor: tag.color,
? (state.filter.tags as IdsTagsQuery) onSelected: (_) => onSelected(),
.includedIds visualDensity: const VisualDensity(vertical: -2),
.contains(tag.id) materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
: false, label: Text(
selectedColor: tag.color, tag.name,
onSelected: (_) => _addTagToFilter(context), style: TextStyle(color: tag.textColor),
visualDensity: const VisualDensity(vertical: -2), ),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, checkmarkColor: tag.textColor,
label: Text( backgroundColor: tag.color,
tag.name, side: BorderSide.none,
style: TextStyle(color: tag.textColor), ),
),
checkmarkColor: tag.textColor,
backgroundColor: tag.color,
side: BorderSide.none,
);
},
), ),
); );
} }
void _addTagToFilter(BuildContext context) {
if (!isClickable) {
return;
}
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
final tagsQuery = cubit.state.filter.tags is IdsTagsQuery
? cubit.state.filter.tags as IdsTagsQuery
: const IdsTagsQuery();
if (tagsQuery.includedIds.contains(tag.id)) {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
tags: tagsQuery.withIdsRemoved([tag.id!]),
),
);
} else {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tag.id!)]),
),
);
}
if (afterTagTapped != null) {
afterTagTapped!();
}
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
} }

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
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/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/model/label_state.dart';
@@ -10,8 +8,10 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.da
class TagsWidget extends StatefulWidget { class TagsWidget extends StatefulWidget {
final Iterable<int> tagIds; final Iterable<int> tagIds;
final bool isMultiLine; final bool isMultiLine;
final void Function()? afterTagTapped; final VoidCallback? afterTagTapped;
final void Function(int tagId) onTagSelected;
final bool isClickable; final bool isClickable;
final bool Function(int id) isSelectedPredicate;
const TagsWidget({ const TagsWidget({
Key? key, Key? key,
@@ -19,6 +19,8 @@ class TagsWidget extends StatefulWidget {
this.afterTagTapped, this.afterTagTapped,
this.isMultiLine = true, this.isMultiLine = true,
this.isClickable = true, this.isClickable = true,
required this.isSelectedPredicate,
required this.onTagSelected,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -37,6 +39,8 @@ class _TagsWidgetState extends State<TagsWidget> {
tag: state.getLabel(id)!, tag: state.getLabel(id)!,
afterTagTapped: widget.afterTagTapped, afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable, isClickable: widget.isClickable,
isSelected: widget.isSelectedPredicate(id),
onSelected: () => widget.onTagSelected(id),
), ),
) )
.toList(); .toList();

View File

@@ -1,6 +1,5 @@
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/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.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';
@@ -218,7 +217,6 @@ class _LabelsPageState extends State<LabelsPage>
builder: (_) => GlobalStateBlocProvider( builder: (_) => GlobalStateBlocProvider(
additionalProviders: [ additionalProviders: [
BlocProvider.value(value: BlocProvider.of<DocumentsCubit>(context)), BlocProvider.value(value: BlocProvider.of<DocumentsCubit>(context)),
BlocProvider.value(value: getIt<PaperlessStatisticsCubit>()),
], ],
child: EditTagPage(tag: tag), child: EditTagPage(tag: tag),
), ),

View File

@@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.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.model.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
@@ -68,15 +71,15 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
builder: (ctxt) => GlobalStateBlocProvider( builder: (ctxt) => GlobalStateBlocProvider(
additionalProviders: [ additionalProviders: [
BlocProvider.value( BlocProvider.value(
value: BlocProvider.of<DocumentsCubit>( value: DocumentDetailsCubit(
context, getIt<DocumentRepository>(),
document,
), ),
), ),
], ],
child: DocumentDetailsPage( child: const DocumentDetailsPage(
documentId: doc.id,
allowEdit: false,
isLabelClickable: false, isLabelClickable: false,
allowEdit: false,
), ),
), ),
), ),
@@ -84,6 +87,8 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
}, },
isSelected: false, isSelected: false,
isAtLeastOneSelected: false, isAtLeastOneSelected: false,
isTagSelectedPredicate: (_) => false,
onTagSelected: (int tag) {},
); );
}, },
), ),

View File

@@ -1,10 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_state.dart';
import 'package:paperless_mobile/features/documents/model/saved_view.model.dart'; import 'package:paperless_mobile/features/documents/model/saved_view.model.dart';
import 'package:paperless_mobile/features/documents/repository/saved_views_repository.dart'; import 'package:paperless_mobile/features/documents/repository/saved_views_repository.dart';
import 'package:injectable/injectable.dart'; import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart';
@singleton @singleton
class SavedViewCubit extends Cubit<SavedViewState> { class SavedViewCubit extends Cubit<SavedViewState> {

View File

@@ -3,7 +3,6 @@ import 'dart:typed_data';
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:flutter_form_builder/flutter_form_builder.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/model/error_message.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';

View File

@@ -8,7 +8,6 @@ import 'package:flutter/foundation.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:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/error_message.dart';
@@ -136,9 +135,6 @@ class _ScannerPageState extends State<ScannerPage>
], ],
child: DocumentUploadPage( child: DocumentUploadPage(
fileBytes: bytes, fileBytes: bytes,
onSuccessfullyConsumed: (_) =>
BlocProvider.of<PaperlessStatisticsCubit>(context)
.updateStatistics(),
), ),
), ),
), ),
@@ -259,9 +255,6 @@ class _ScannerPageState extends State<ScannerPage>
child: DocumentUploadPage( child: DocumentUploadPage(
filename: filename, filename: filename,
fileBytes: fileBytes, fileBytes: fileBytes,
onSuccessfullyConsumed: (_) =>
BlocProvider.of<PaperlessStatisticsCubit>(context)
.updateStatistics(),
), ),
), ),
), ),

View File

@@ -188,7 +188,7 @@
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?", "editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
"settingsPageStorageSettingsLabel": "Speicher", "settingsPageStorageSettingsLabel": "Speicher",
"settingsPageStorageSettingsDescriptionText": "Dateien und Speicherplatz verwalten", "settingsPageStorageSettingsDescriptionText": "Dateien und Speicherplatz verwalten",
"documentUpdateErrorMessage": "Dokument erfolgreich aktualisiert.", "documentUpdateSuccessMessage": "Dokument erfolgreich aktualisiert.",
"errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.", "errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.",
"serverInformationPaperlessVersionText": "Paperless Server-Version", "serverInformationPaperlessVersionText": "Paperless Server-Version",
"errorReportLabel": "MELDEN", "errorReportLabel": "MELDEN",

View File

@@ -189,7 +189,7 @@
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", "editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
"settingsPageStorageSettingsLabel": "Storage", "settingsPageStorageSettingsLabel": "Storage",
"settingsPageStorageSettingsDescriptionText": "Manage files and storage space", "settingsPageStorageSettingsDescriptionText": "Manage files and storage space",
"documentUpdateErrorMessage": "Document successfully updated.", "documentUpdateSuccessMessage": "Document successfully updated.",
"errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.", "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.",
"serverInformationPaperlessVersionText": "Paperless server version", "serverInformationPaperlessVersionText": "Paperless server version",
"errorReportLabel": "REPORT", "errorReportLabel": "REPORT",

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -11,7 +12,6 @@ import 'package:intl/intl.dart';
import 'package:intl/intl_standalone.dart'; import 'package:intl/intl_standalone.dart';
import 'package:package_info_plus/package_info_plus.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/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/global/asset_images.dart'; import 'package:paperless_mobile/core/global/asset_images.dart';
@@ -51,6 +51,13 @@ void main() async {
await getIt<ApplicationSettingsCubit>().initialize(); await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize(); await getIt<AuthenticationCubit>().initialize();
// Preload asset images
// WARNING: This seems to bloat up the app up to almost 200mb!
// await Future.forEach<AssetImage>(
// AssetImages.values.map((e) => e.image),
// (img) => loadImage(img),
// );
runApp(const PaperlessMobileEntrypoint()); runApp(const PaperlessMobileEntrypoint());
} }
@@ -70,7 +77,6 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
BlocProvider.value(value: getIt<ConnectivityCubit>()), BlocProvider.value(value: getIt<ConnectivityCubit>()),
BlocProvider.value(value: getIt<AuthenticationCubit>()), BlocProvider.value(value: getIt<AuthenticationCubit>()),
BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()), BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()),
BlocProvider.value(value: getIt<PaperlessStatisticsCubit>()),
BlocProvider.value(value: getIt<ApplicationSettingsCubit>()), BlocProvider.value(value: getIt<ApplicationSettingsCubit>()),
], ],
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>( child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
@@ -229,7 +235,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
if (authentication.isAuthenticated) { if (authentication.isAuthenticated) {
return GlobalStateBlocProvider( return GlobalStateBlocProvider(
additionalProviders: [ additionalProviders: [
BlocProvider.value(value: getIt<PaperlessStatisticsCubit>()),
BlocProvider.value(value: getIt<DocumentsCubit>()), BlocProvider.value(value: getIt<DocumentsCubit>()),
], ],
child: const HomePage(), child: const HomePage(),

View File

@@ -1,9 +1,13 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.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/model/error_message.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -120,3 +124,35 @@ String formatLocalDate(BuildContext context, DateTime dateTime) {
String extractFilenameFromPath(String path) { String extractFilenameFromPath(String path) {
return path.split(RegExp('[./]')).reversed.skip(1).first; return path.split(RegExp('[./]')).reversed.skip(1).first;
} }
// Taken from https://github.com/flutter/flutter/issues/26127#issuecomment-782083060
Future<void> loadImage(ImageProvider provider) {
final config = ImageConfiguration(
bundle: rootBundle,
devicePixelRatio: window.devicePixelRatio,
platform: defaultTargetPlatform,
);
final Completer<void> completer = Completer();
final ImageStream stream = provider.resolve(config);
late final ImageStreamListener listener;
listener = ImageStreamListener((ImageInfo image, bool sync) {
debugPrint("Image ${image.debugLabel} finished loading");
completer.complete();
stream.removeListener(listener);
}, onError: (dynamic exception, StackTrace? stackTrace) {
completer.complete();
stream.removeListener(listener);
FlutterError.reportError(FlutterErrorDetails(
context: ErrorDescription('image failed to load'),
library: 'image resource service',
exception: exception,
stack: stackTrace,
silent: true,
));
});
stream.addListener(listener);
return completer.future;
}

View File

@@ -1,6 +1,5 @@
name: paperless_mobile name: paperless_mobile
description: description: Application to conveniently scan and share documents with a paperless-ng
Application to conveniently scan and share documents with a paperless-ng
server. server.
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
@@ -139,6 +138,8 @@ flutter:
# see https://flutter.dev/custom-fonts/#from-packages # see https://flutter.dev/custom-fonts/#from-packages
flutter_intl: flutter_intl:
enabled: true enabled: true
localizely:
project_id: 84b4144d-a628-4ba6-a8d0-4f9917444057
flutter_native_splash: flutter_native_splash:
image: assets/logos/paperless_logo_green.png image: assets/logos/paperless_logo_green.png

View File

@@ -90,7 +90,7 @@ void main() {
addedDateBefore: DateTime.parse("2022-09-26"), addedDateBefore: DateTime.parse("2022-09-26"),
addedDateAfter: DateTime.parse("2000-01-01"), addedDateAfter: DateTime.parse("2000-01-01"),
sortField: SortField.created, sortField: SortField.created,
sortOrder: SortOrder.ascending, sortOrder: SortOrder.descending,
queryText: "Never gonna give you up", queryText: "Never gonna give you up",
queryType: QueryType.extended, queryType: QueryType.extended,
), ),
@@ -106,7 +106,7 @@ void main() {
"show_on_dashboard": false, "show_on_dashboard": false,
"show_in_sidebar": false, "show_in_sidebar": false,
"sort_field": SortField.created.name, "sort_field": SortField.created.name,
"sort_reverse": false, "sort_reverse": true,
"filter_rules": [], "filter_rules": [],
}).toDocumentFilter(), }).toDocumentFilter(),
equals(DocumentFilter.initial), equals(DocumentFilter.initial),
@@ -121,7 +121,7 @@ void main() {
"show_on_dashboard": false, "show_on_dashboard": false,
"show_in_sidebar": false, "show_in_sidebar": false,
"sort_field": SortField.created.name, "sort_field": SortField.created.name,
"sort_reverse": false, "sort_reverse": true,
"filter_rules": [ "filter_rules": [
{ {
'rule_type': FilterRule.correspondentRule, 'rule_type': FilterRule.correspondentRule,
@@ -185,7 +185,7 @@ void main() {
showOnDashboard: false, showOnDashboard: false,
showInSidebar: false, showInSidebar: false,
sortField: SortField.added, sortField: SortField.added,
sortReverse: true, sortReverse: false,
filterRules: [ filterRules: [
FilterRule(FilterRule.correspondentRule, "1"), FilterRule(FilterRule.correspondentRule, "1"),
FilterRule(FilterRule.documentTypeRule, "2"), FilterRule(FilterRule.documentTypeRule, "2"),
@@ -232,7 +232,7 @@ void main() {
showOnDashboard: false, showOnDashboard: false,
showInSidebar: false, showInSidebar: false,
sortField: SortField.created, sortField: SortField.created,
sortReverse: false, sortReverse: true,
filterRules: [], filterRules: [],
), ),
), ),
@@ -248,7 +248,7 @@ void main() {
storagePath: StoragePathQuery.notAssigned(), storagePath: StoragePathQuery.notAssigned(),
tags: OnlyNotAssignedTagsQuery(), tags: OnlyNotAssignedTagsQuery(),
sortField: SortField.created, sortField: SortField.created,
sortOrder: SortOrder.descending, sortOrder: SortOrder.ascending,
), ),
name: "test_name", name: "test_name",
showInSidebar: false, showInSidebar: false,