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

@@ -15,11 +15,6 @@ class DocumentsCubit extends Cubit<DocumentsState> {
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 {
await documentRepository.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)),
@@ -40,8 +35,13 @@ class DocumentsCubit extends Cubit<DocumentsState> {
await reload();
}
Future<void> update(DocumentModel document) async {
await documentRepository.update(document);
Future<void> update(
DocumentModel document, [
bool updateRemote = true,
]) async {
if (updateRemote) {
await documentRepository.update(document);
}
await reload();
}
@@ -83,13 +83,6 @@ class DocumentsCubit extends Cubit<DocumentsState> {
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.
/// Use [DocumentsCubit.loadMore] to load more data.

View File

@@ -1,52 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.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/repository/saved_views_repository.dart';
import 'package:injectable/injectable.dart';
@singleton
class SavedViewCubit extends Cubit<SavedViewState> {
SavedViewCubit() : super(SavedViewState(value: {}));
void selectView(SavedView? view) {
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
}
Future<SavedView> add(SavedView view) async {
final savedView = await getIt<SavedViewsRepository>().save(view);
emit(
SavedViewState(
value: {...state.value, savedView.id!: savedView},
selectedSavedViewId: state.selectedSavedViewId,
),
);
return savedView;
}
Future<int> remove(SavedView view) async {
final id = await getIt<SavedViewsRepository>().delete(view);
final newValue = {...state.value};
newValue.removeWhere((key, value) => key == id);
emit(
SavedViewState(
value: newValue,
selectedSavedViewId: view.id == state.selectedSavedViewId
? null
: state.selectedSavedViewId,
),
);
return id;
}
Future<void> initialize() async {
final views = await getIt<SavedViewsRepository>().getAll();
final values = {for (var element in views) element.id!: element};
emit(SavedViewState(value: values));
}
void resetSelection() {
emit(SavedViewState(value: state.value));
}
}

View File

@@ -1,15 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:paperless_mobile/features/documents/model/saved_view.model.dart';
class SavedViewState with EquatableMixin {
final Map<int, SavedView> value;
final int? selectedSavedViewId;
SavedViewState({
required this.value,
this.selectedSavedViewId,
});
@override
List<Object?> get props => [value, selectedSavedViewId];
}

View File

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

View File

@@ -2,8 +2,6 @@
import 'package:equatable/equatable.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 {
static const idKey = 'id';

View File

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

View File

@@ -1,494 +0,0 @@
import 'dart:developer' as dev;
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.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/repository/document_repository.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.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/document_preview.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
class DocumentDetailsPage extends StatefulWidget {
final int documentId;
final bool allowEdit;
final bool isLabelClickable;
const DocumentDetailsPage({
Key? key,
required this.documentId,
this.allowEdit = true,
this.isLabelClickable = true,
}) : super(key: key);
@override
State<DocumentDetailsPage> createState() => _DocumentDetailsPageState();
}
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
static final DateFormat _detailedDateFormat =
DateFormat("MMM d, yyyy HH:mm:ss");
bool _isDownloadPending = false;
@override
Widget build(BuildContext context) {
return BlocBuilder<DocumentsCubit, DocumentsState>(
// buildWhen required because rebuild would happen after delete causing error.
buildWhen: (previous, current) {
return current.documents
.where((element) => element.id == widget.documentId)
.isNotEmpty;
},
builder: (context, state) {
final document =
state.documents.where((doc) => doc.id == widget.documentId).first;
return DefaultTabController(
length: 3,
child: Scaffold(
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
floatingActionButton: widget.allowEdit
? FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(document),
)
: null,
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
icon: const Icon(Icons.delete),
onPressed:
widget.allowEdit ? () => _onDelete(document) : null,
).padded(const EdgeInsets.symmetric(horizontal: 4)),
IconButton(
icon: const Icon(Icons.download),
onPressed:
Platform.isAndroid ? () => _onDownload(document) : null,
).padded(const EdgeInsets.only(right: 4)),
IconButton(
icon: const Icon(Icons.open_in_new),
onPressed: () => _onOpen(document),
).padded(const EdgeInsets.only(right: 4)),
IconButton(
icon: const Icon(Icons.share),
onPressed: () => _onShare(document),
),
],
),
),
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...
),
onPressed: () => Navigator.pop(context),
),
floating: true,
pinned: true,
expandedHeight: 200.0,
flexibleSpace: DocumentPreview(
id: document.id,
fit: BoxFit.cover,
),
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(
S.of(context).documentDetailsPageTabContentLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
Tab(
child: Text(
S.of(context).documentDetailsPageTabMetaDataLabel,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
],
),
),
),
],
body: TabBarView(
children: [
_buildDocumentOverview(
document, state.filter.titleAndContentMatchString),
_buildDocumentContentView(
document, state.filter.titleAndContentMatchString),
_buildDocumentMetaDataView(document),
].padded(),
),
),
),
);
},
);
}
Widget _buildDocumentMetaDataView(DocumentModel document) {
return FutureBuilder<DocumentMetaData>(
future: getIt<DocumentRepository>().getMetaData(document),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final meta = snapshot.data!;
return ListView(
children: [
_DetailsItem.text(_detailedDateFormat.format(document.modified),
label: S.of(context).documentModifiedPropertyLabel,
context: context),
_separator(),
_DetailsItem.text(_detailedDateFormat.format(document.added),
label: S.of(context).documentAddedPropertyLabel,
context: context),
_separator(),
_DetailsItem(
label: S.of(context).documentArchiveSerialNumberPropertyLongLabel,
content: document.archiveSerialNumber != null
? Text(document.archiveSerialNumber.toString())
: OutlinedButton(
child: Text(S
.of(context)
.documentDetailsPageAssignAsnButtonLabel),
onPressed:
widget.allowEdit ? () => _assignAsn(document) : null,
),
),
_separator(),
_DetailsItem.text(
meta.mediaFilename,
context: context,
label: S.of(context).documentMetaDataMediaFilenamePropertyLabel,
),
_separator(),
_DetailsItem.text(
meta.originalChecksum,
context: context,
label: S.of(context).documentMetaDataChecksumLabel,
),
_separator(),
_DetailsItem.text(formatBytes(meta.originalSize, 2),
label: S.of(context).documentMetaDataOriginalFileSizeLabel,
context: context),
_separator(),
_DetailsItem.text(
meta.originalMimeType,
label: S.of(context).documentMetaDataOriginalMimeTypeLabel,
context: context,
),
_separator(),
],
);
},
);
}
Future<void> _assignAsn(DocumentModel document) async {
try {
await BlocProvider.of<DocumentsCubit>(context).assignAsn(document);
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
Widget _buildDocumentContentView(DocumentModel document, String? match) {
return SingleChildScrollView(
child: _DetailsItem(
content: HighlightedText(
text: document.content ?? "",
highlights: match == null ? [] : match.split(" "),
style: Theme.of(context).textTheme.bodyText2,
caseSensitive: false,
),
label: S.of(context).documentDetailsPageTabContentLabel,
),
);
}
Widget _buildDocumentOverview(DocumentModel document, String? match) {
return ListView(
children: [
_DetailsItem(
content: HighlightedText(
text: document.title,
highlights: match?.split(" ") ?? <String>[],
),
label: S.of(context).documentTitlePropertyLabel,
),
_separator(),
_DetailsItem.text(
DateFormat.yMMMd(Localizations.localeOf(context).toLanguageTag())
.format(document.created),
context: context,
label: S.of(context).documentCreatedPropertyLabel,
),
_separator(),
_DetailsItem(
content: DocumentTypeWidget(
isClickable: widget.isLabelClickable,
documentTypeId: document.documentType,
afterSelected: () {
Navigator.pop(context);
},
),
label: S.of(context).documentDocumentTypePropertyLabel,
),
_separator(),
_DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel,
content: CorrespondentWidget(
isClickable: widget.isLabelClickable,
correspondentId: document.correspondent,
afterSelected: () {
Navigator.pop(context);
},
),
),
_separator(),
_DetailsItem(
label: S.of(context).documentStoragePathPropertyLabel,
content: StoragePathWidget(
isClickable: widget.isLabelClickable,
pathId: document.storagePath,
afterSelected: () {
Navigator.pop(context);
},
),
),
_separator(),
_DetailsItem(
label: S.of(context).documentTagsPropertyLabel,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: widget.isLabelClickable,
tagIds: document.tags,
),
),
),
// _separator(),
// FutureBuilder<List<SimilarDocumentModel>>(
// future: getIt<DocumentRepository>().findSimilar(document.id),
// builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return CircularProgressIndicator();
// }
// return ExpansionTile(
// tilePadding: const EdgeInsets.symmetric(horizontal: 8.0),
// title: Text(
// S.of(context).documentDetailsPageSimilarDocumentsLabel,
// style:
// Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
// ),
// children: snapshot.data!
// .map((e) => DocumentListItem(
// document: e,
// onTap: (doc) {},
// isSelected: false,
// isAtLeastOneSelected: false))
// .toList(),
// );
// }),
],
);
}
Widget _separator() {
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 {
if (!Platform.isAndroid) {
showSnackBar(
context, "This feature is currently only supported on Android!");
return;
}
setState(() => _isDownloadPending = true);
getIt<DocumentRepository>().download(document).then((bytes) async {
final Directory dir = (await getExternalStorageDirectories(
type: StorageDirectory.downloads))!
.first;
String filePath = "${dir.path}/${document.originalFileName}";
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
await File(filePath).writeAsBytes(bytes);
setState(() => _isDownloadPending = false);
dev.log("File downloaded to $filePath");
});
}
///
/// Downloads file to temporary directory, from which it can then be shared.
///
Future<void> _onShare(DocumentModel document) async {
Uint8List documentBytes =
await getIt<DocumentRepository>().download(document);
final dir = await getTemporaryDirectory();
final String path = "${dir.path}/${document.originalFileName}";
await File(path).writeAsBytes(documentBytes);
Share.shareXFiles(
[
XFile(
path,
name: document.originalFileName,
mimeType: "application/pdf",
lastModified: document.modified,
)
],
subject: document.title,
);
}
void _onDelete(DocumentModel document) async {
final delete = await showDialog(
context: context,
builder: (context) =>
DeleteDocumentConfirmationDialog(document: document),
) ??
false;
if (delete) {
try {
await BlocProvider.of<DocumentsCubit>(context).remove(document);
showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} finally {
Navigator.pop(context);
}
}
}
Future<void> _onOpen(DocumentModel document) async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes: getIt<DocumentRepository>().getPreview(document.id),
),
),
);
}
static String formatBytes(int bytes, int decimals) {
if (bytes <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var i = (log(bytes) / log(1024)).floor();
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) +
' ' +
suffixes[i];
}
}
class _DetailsItem extends StatelessWidget {
final String label;
final Widget content;
const _DetailsItem({Key? key, required this.label, required this.content})
: super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context)
.textTheme
.headline5
?.copyWith(fontWeight: FontWeight.bold),
),
content,
],
),
);
}
_DetailsItem.text(
String text, {
required this.label,
required BuildContext context,
}) : content = Text(text, style: Theme.of(context).textTheme.bodyText2);
}
class ColoredTabBar extends Container implements PreferredSizeWidget {
ColoredTabBar({
super.key,
required this.backgroundColor,
required this.tabBar,
});
final TabBar tabBar;
final Color backgroundColor;
@override
Size get preferredSize => tabBar.preferredSize;
@override
Widget build(BuildContext context) => Container(
color: backgroundColor,
child: tabBar,
);
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
@@ -33,7 +34,13 @@ import 'package:paperless_mobile/util.dart';
class DocumentEditPage extends StatefulWidget {
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
State<DocumentEditPage> createState() => _DocumentEditPageState();
@@ -66,7 +73,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
onPressed: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value;
final updatedDocument = widget.document.copyWith(
var updatedDocument = widget.document.copyWith(
title: values[fkTitle],
created: values[fkCreatedDate],
overwriteDocumentType: true,
@@ -81,15 +88,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
setState(() {
_isSubmitLoading = true;
});
bool wasUpdated = false;
try {
await getIt<DocumentsCubit>().update(updatedDocument);
showSnackBar(context, S.of(context).documentUpdateErrorMessage);
wasUpdated = true;
await widget.onEdit(updatedDocument);
showSnackBar(context, S.of(context).documentUpdateSuccessMessage);
} on ErrorMessage catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/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/grid/document_grid.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) {
case ViewType.list:
child = DocumentListView(
onTap: _openDocumentDetails,
onTap: _openDetails,
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected,
onTagSelected: _addTagToFilter,
);
break;
case ViewType.grid:
child = DocumentGridView(
onTap: _openDocumentDetails,
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected);
onTap: _openDetails,
state: state,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection:
connectivityState == ConnectivityState.connected,
onTagSelected: (int tagId) => _addTagToFilter,
);
break;
}
@@ -222,26 +228,63 @@ class _DocumentsPageState extends State<DocumentsPage> {
);
}
void _openDocumentDetails(DocumentModel model) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(value: BlocProvider.of<DocumentsCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<CorrespondentCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<DocumentTypeCubit>(context)),
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(context)),
BlocProvider.value(
value: BlocProvider.of<PaperlessStatisticsCubit>(context)),
],
child: DocumentDetailsPage(documentId: model.id),
),
Future<void> _openDetails(DocumentModel document) async {
await Navigator.of(context).push<DocumentModel?>(
_buildDetailsPageRoute(document),
);
BlocProvider.of<DocumentsCubit>(context).reload();
}
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
DocumentModel document) {
return MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: BlocProvider.of<DocumentsCubit>(context),
),
BlocProvider.value(
value: BlocProvider.of<CorrespondentCubit>(context),
),
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/features/documents/bloc/documents_cubit.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/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
@@ -23,7 +23,7 @@ class DocumentsEmptyState extends StatelessWidget {
title: S.of(context).documentsPageEmptyStateOopsText,
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
bottomChild: state.filter != DocumentFilter.initial
? ElevatedButton(
? TextButton(
onPressed: () async {
await BlocProvider.of<DocumentsCubit>(context).updateFilter();
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/features/documents/bloc/documents_state.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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
@@ -11,6 +12,7 @@ class DocumentGridView extends StatelessWidget {
final PagingController<int, DocumentModel> pagingController;
final DocumentsState state;
final bool hasInternetConnection;
final void Function(int tagId) onTagSelected;
const DocumentGridView({
super.key,
@@ -19,6 +21,7 @@ class DocumentGridView extends StatelessWidget {
required this.state,
required this.onSelected,
required this.hasInternetConnection,
required this.onTagSelected,
});
@override
Widget build(BuildContext context) {
@@ -38,6 +41,14 @@ class DocumentGridView extends StatelessWidget {
isSelected: state.selection.contains(item),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
);
},
noItemsFoundIndicatorBuilder: (context) =>

View File

@@ -12,6 +12,8 @@ class DocumentGridItem extends StatelessWidget {
final void Function(DocumentModel) onTap;
final void Function(DocumentModel) onSelected;
final bool isAtLeastOneSelected;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId) onTagSelected;
const DocumentGridItem({
Key? key,
@@ -20,6 +22,8 @@ class DocumentGridItem extends StatelessWidget {
required this.onSelected,
required this.isSelected,
required this.isAtLeastOneSelected,
required this.isTagSelectedPredicate,
required this.onTagSelected,
}) : super(key: key);
@override
@@ -65,6 +69,8 @@ class DocumentGridItem extends StatelessWidget {
TagsWidget(
tagIds: document.tags,
isMultiLine: false,
isSelectedPredicate: isTagSelectedPredicate,
onTagSelected: onTagSelected,
),
const Spacer(),
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/features/documents/bloc/documents_state.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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
class DocumentListView extends StatelessWidget {
final void Function(DocumentModel) onTap;
@@ -14,6 +16,8 @@ class DocumentListView extends StatelessWidget {
final DocumentsState state;
final bool hasInternetConnection;
final bool isLabelClickable;
final void Function(int tagId) onTagSelected;
const DocumentListView({
super.key,
required this.onTap,
@@ -22,6 +26,7 @@ class DocumentListView extends StatelessWidget {
required this.onSelected,
required this.hasInternetConnection,
this.isLabelClickable = true,
required this.onTagSelected,
});
@override
@@ -38,6 +43,14 @@ class DocumentListView extends StatelessWidget {
isSelected: state.selection.contains(document),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
);
},
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection

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/view/widgets/document_preview.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';
class DocumentListItem extends StatelessWidget {
@@ -12,6 +13,9 @@ class DocumentListItem extends StatelessWidget {
final bool isSelected;
final bool isAtLeastOneSelected;
final bool isLabelClickable;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId) onTagSelected;
const DocumentListItem({
Key? key,
@@ -21,6 +25,8 @@ class DocumentListItem extends StatelessWidget {
required this.isSelected,
required this.isAtLeastOneSelected,
this.isLabelClickable = true,
required this.isTagSelectedPredicate,
required this.onTagSelected,
}) : super(key: key);
@override
@@ -62,6 +68,8 @@ class DocumentListItem extends StatelessWidget {
isClickable: isLabelClickable,
tagIds: document.tags,
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/features/documents/bloc/documents_cubit.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_filter.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/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/util.dart';

View File

@@ -1,10 +1,6 @@
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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class ConfirmDeleteSavedViewDialog extends StatelessWidget {
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/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_state.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/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/util.dart';