Removed suggestions from inbox, added translations, added paging to inbox, visual updates, changed default matching algorithm to auto

This commit is contained in:
Anton Stubenbord
2023-01-20 00:34:18 +01:00
parent bfbf0a6f0e
commit f9dfddf704
56 changed files with 1748 additions and 766 deletions

View File

@@ -5,19 +5,21 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
class DocumentsCubit extends HydratedCubit<DocumentsState>
with DocumentsPagingMixin {
@override
final PaperlessDocumentsApi api;
class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
final PaperlessDocumentsApi _api;
final SavedViewRepository _savedViewRepository;
DocumentsCubit(this._api, this._savedViewRepository)
: super(const DocumentsState()) {
hydrate();
}
DocumentsCubit(this.api, this._savedViewRepository)
: super(const DocumentsState());
Future<void> bulkRemove(List<DocumentModel> documents) async {
log("[DocumentsCubit] bulkRemove");
await _api.bulkAction(
await api.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)),
);
await reload();
@@ -29,7 +31,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
Iterable<int> removeTags = const [],
}) async {
log("[DocumentsCubit] bulkEditTags");
await _api.bulkAction(BulkModifyTagsAction(
await api.bulkAction(BulkModifyTagsAction(
documents.map((doc) => doc.id),
addTags: addTags,
removeTags: removeTags,
@@ -37,132 +39,6 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
await reload();
}
Future<void> update(
DocumentModel document, [
bool updateRemote = true,
]) async {
log("[DocumentsCubit] update");
if (updateRemote) {
await _api.update(document);
}
await reload();
}
Future<void> load() async {
log("[DocumentsCubit] load");
emit(state.copyWith(isLoading: true));
try {
final result = await _api.findAll(state.filter);
emit(state.copyWith(
isLoading: false,
hasLoaded: true,
value: [...state.value, result],
));
} finally {
emit(state.copyWith(isLoading: false));
}
}
Future<void> reload() async {
log("[DocumentsCubit] reload");
emit(state.copyWith(isLoading: true));
try {
final filter = state.filter.copyWith(page: 1);
final result = await _api.findAll(filter);
emit(state.copyWith(
hasLoaded: true,
value: [result],
isLoading: false,
filter: filter,
));
} finally {
emit(state.copyWith(isLoading: false));
}
}
Future<void> _bulkReloadDocuments() async {
emit(state.copyWith(isLoading: true));
try {
final result = await _api.findAll(
state.filter.copyWith(
page: 1,
pageSize: state.documents.length,
),
);
emit(DocumentsState(
hasLoaded: true,
value: [result],
filter: state.filter,
isLoading: false,
));
} finally {
emit(state.copyWith(isLoading: false));
}
}
Future<void> loadMore() async {
log("[DocumentsCubit] loadMore");
if (state.isLastPageLoaded) {
return;
}
emit(state.copyWith(isLoading: true));
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
try {
final result = await _api.findAll(newFilter);
emit(
DocumentsState(
hasLoaded: true,
value: [...state.value, result],
filter: newFilter,
isLoading: false,
),
);
} finally {
emit(state.copyWith(isLoading: false));
}
}
///
/// Updates document filter and automatically reloads documents. Always resets page to 1.
/// Use [loadMore] to load more data.
Future<void> updateFilter({
final DocumentFilter filter = DocumentFilter.initial,
}) async {
log("[DocumentsCubit] updateFilter");
try {
emit(state.copyWith(isLoading: true));
final result = await _api.findAll(filter.copyWith(page: 1));
emit(
DocumentsState(
filter: filter,
value: [result],
hasLoaded: true,
isLoading: false,
),
);
} finally {
emit(state.copyWith(isLoading: false));
}
}
Future<void> resetFilter() {
log("[DocumentsCubit] resetFilter");
final filter = DocumentFilter.initial.copyWith(
sortField: state.filter.sortField,
sortOrder: state.filter.sortOrder,
);
return updateFilter(filter: filter);
}
///
/// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
///
Future<void> updateCurrentFilter(
final DocumentFilter Function(DocumentFilter) transformFn,
) async =>
updateFilter(filter: transformFn(state.filter));
void toggleDocumentSelection(DocumentModel model) {
log("[DocumentsCubit] toggleSelection");
if (state.selectedIds.contains(model.id)) {
@@ -185,12 +61,6 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
emit(state.copyWith(selection: []));
}
@override
void onChange(Change<DocumentsState> change) {
super.onChange(change);
log("[DocumentsCubit] state changed: ${change.currentState.selection.map((e) => e.id).join(",")} to ${change.nextState.selection.map((e) => e.id).join(",")}");
}
void reset() {
log("[DocumentsCubit] reset");
emit(const DocumentsState());
@@ -204,7 +74,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
if (filter == null) {
return;
}
final results = await _api.findAll(filter.copyWith(page: 1));
final results = await api.findAll(filter.copyWith(page: 1));
emit(
DocumentsState(
filter: filter,
@@ -220,7 +90,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
}
Future<Iterable<String>> autocomplete(String query) async {
final res = await _api.autocomplete(query);
final res = await api.autocomplete(query);
return res;
}

View File

@@ -1,72 +1,25 @@
import 'dart:developer';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
class DocumentsState extends Equatable {
final bool isLoading;
final bool hasLoaded;
final DocumentFilter filter;
final List<PagedSearchResult<DocumentModel>> value;
class DocumentsState extends DocumentsPagedState {
final int? selectedSavedViewId;
@JsonKey(ignore: true)
final List<DocumentModel> selection;
const DocumentsState({
this.hasLoaded = false,
this.isLoading = false,
this.value = const [],
this.filter = const DocumentFilter(),
this.selection = const [],
this.selectedSavedViewId,
super.value = const [],
super.filter = const DocumentFilter(),
super.hasLoaded = false,
super.isLoading = false,
});
List<int> get selectedIds => selection.map((e) => e.id).toList();
int get currentPageNumber {
return filter.page;
}
int? get nextPageNumber {
return isLastPageLoaded ? null : currentPageNumber + 1;
}
int get count {
if (value.isEmpty) {
return 0;
}
return value.first.count;
}
bool get isLastPageLoaded {
if (!hasLoaded) {
return false;
}
if (value.isNotEmpty) {
return value.last.next == null;
}
return true;
}
int inferPageCount({required int pageSize}) {
if (!hasLoaded) {
return 100000;
}
if (value.isEmpty) {
return 0;
}
return value.first.inferPageCount(pageSize: pageSize);
}
List<DocumentModel> get documents {
return value.fold(
[], (previousValue, element) => [...previousValue, ...element.results]);
}
DocumentsState copyWith({
bool overwrite = false,
bool? hasLoaded,
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
@@ -118,4 +71,19 @@ class DocumentsState extends Equatable {
filter: DocumentFilter.fromJson(json['filter']),
);
}
@override
DocumentsState copyWithPaged({
bool? hasLoaded,
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
}) {
return copyWith(
filter: filter,
hasLoaded: hasLoaded,
isLoading: isLoading,
value: value,
);
}
}

View File

@@ -16,8 +16,6 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -45,6 +43,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
bool _isSubmitLoading = false;
late final FieldSuggestions _filteredSuggestions;
@override
void initState() {
super.initState();
_filteredSuggestions = widget.suggestions
.documentDifference(context.read<EditDocumentCubit>().state.document);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<EditDocumentCubit, EditDocumentState>(
@@ -99,25 +106,35 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
excludeAllowed: false,
name: fkTags,
selectableOptions: state.tags,
suggestions: widget.suggestions.hasSuggestedTags
suggestions: _filteredSuggestions.tags
.toSet()
.difference(state.document.tags.toSet())
.isNotEmpty
? _buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.storagePaths,
itemBuilder: (context, itemData) => ActionChip(
label: Text(state.tags[itemData]!.name),
onPressed: () {
final currentTags = _formKey.currentState
?.fields[fkTags] as TagsQuery;
if (currentTags is IdsTagsQuery) {
_formKey.currentState?.fields[fkTags]
?.didChange((IdsTagsQuery.fromIds(
[...currentTags.ids, itemData])));
} else {
_formKey.currentState?.fields[fkTags]
?.didChange(
(IdsTagsQuery.fromIds([itemData])));
}
},
),
suggestions: _filteredSuggestions.tags,
itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!;
return ActionChip(
label: Text(
tag.name,
style: TextStyle(color: tag.textColor),
),
backgroundColor: tag.color,
onPressed: () {
final currentTags = _formKey.currentState
?.fields[fkTags]?.value as TagsQuery;
if (currentTags is IdsTagsQuery) {
_formKey.currentState?.fields[fkTags]
?.didChange((IdsTagsQuery.fromIds(
{...currentTags.ids, itemData})));
} else {
_formKey.currentState?.fields[fkTags]
?.didChange((IdsTagsQuery.fromIds(
{itemData})));
}
},
);
},
)
: null,
).padded(),
@@ -151,15 +168,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
),
if (widget.suggestions.hasSuggestedStoragePaths)
_buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.storagePaths,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkStoragePath]
?.didChange((IdQueryParameter.fromId(itemData))),
),
),
],
);
}
@@ -182,9 +190,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
),
if (widget.suggestions.hasSuggestedCorrespondents)
if (_filteredSuggestions.hasSuggestedCorrespondents)
_buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.correspondents,
suggestions: _filteredSuggestions.correspondents,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkCorrespondent]
@@ -217,9 +225,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined),
),
if (widget.suggestions.hasSuggestedDocumentTypes)
if (_filteredSuggestions.hasSuggestedDocumentTypes)
_buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.documentTypes,
suggestions: _filteredSuggestions.documentTypes,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
@@ -236,13 +244,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
var mergedDocument = document.copyWith(
title: values[fkTitle],
created: values[fkCreatedDate],
overwriteDocumentType: true,
documentType: (values[fkDocumentType] as IdQueryParameter).id,
overwriteCorrespondent: true,
correspondent: (values[fkCorrespondent] as IdQueryParameter).id,
overwriteStoragePath: true,
storagePath: (values[fkStoragePath] as IdQueryParameter).id,
overwriteTags: true,
documentType: () => (values[fkDocumentType] as IdQueryParameter).id,
correspondent: () => (values[fkCorrespondent] as IdQueryParameter).id,
storagePath: () => (values[fkStoragePath] as IdQueryParameter).id,
tags: (values[fkTags] as IdsTagsQuery).includedIds,
);
setState(() {
@@ -288,9 +292,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
initialEntryMode: DatePickerEntryMode.calendar,
),
if (widget.suggestions.hasSuggestedDates)
if (_filteredSuggestions.hasSuggestedDates)
_buildSuggestionsSkeleton<DateTime>(
suggestions: widget.suggestions.dates,
suggestions: _filteredSuggestions.dates,
itemBuilder: (context, itemData) => ActionChip(
label: Text(DateFormat.yMMMd().format(itemData)),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]

View File

@@ -63,13 +63,13 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
_scrollController
..addListener(_listenForScrollChanges)
..addListener(_listenForLoadDataTrigger);
..addListener(_listenForLoadNewData);
}
void _listenForLoadDataTrigger() {
void _listenForLoadNewData() {
final currState = context.read<DocumentsCubit>().state;
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
_scrollController.position.maxScrollExtent * 0.75 &&
!currState.isLoading &&
!currState.isLastPageLoaded) {
_loadNewPage();
@@ -173,7 +173,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
bottom: PreferredSize(
preferredSize: const Size.fromHeight(
linearProgressIndicatorHeight),
child: state.isLoading
child: state.isLoading && state.hasLoaded
? const LinearProgressIndicator()
: const SizedBox(height: 4.0),
),