mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-11 00:07:59 -06:00
Removed suggestions from inbox, added translations, added paging to inbox, visual updates, changed default matching algorithm to auto
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user