mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 10:08:02 -06:00
feat: extract snippets into widgets, code cleanup document details
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
|
||||
import 'paged_documents_state.dart';
|
||||
|
||||
///
|
||||
/// Mixin which can be used on cubits that handle documents.
|
||||
/// This implements all paging and filtering logic.
|
||||
///
|
||||
mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||
on BlocBase<State> {
|
||||
PaperlessDocumentsApi get api;
|
||||
DocumentChangedNotifier get notifier;
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (state.isLastPageLoaded) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWithPaged(isLoading: true));
|
||||
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
|
||||
try {
|
||||
final result = await api.findAll(newFilter);
|
||||
emit(state.copyWithPaged(
|
||||
hasLoaded: true,
|
||||
filter: newFilter,
|
||||
value: [...state.value, result],
|
||||
));
|
||||
} finally {
|
||||
emit(state.copyWithPaged(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 {
|
||||
try {
|
||||
emit(state.copyWithPaged(isLoading: true));
|
||||
final result = await api.findAll(filter.copyWith(page: 1));
|
||||
|
||||
emit(state.copyWithPaged(
|
||||
filter: filter,
|
||||
value: [result],
|
||||
hasLoaded: true,
|
||||
));
|
||||
} finally {
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// 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));
|
||||
|
||||
Future<void> resetFilter() {
|
||||
final filter = DocumentFilter.initial.copyWith(
|
||||
sortField: state.filter.sortField,
|
||||
sortOrder: state.filter.sortOrder,
|
||||
);
|
||||
return updateFilter(filter: filter);
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
emit(state.copyWithPaged(isLoading: true));
|
||||
try {
|
||||
final filter = state.filter.copyWith(page: 1);
|
||||
final result = await api.findAll(filter);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWithPaged(
|
||||
hasLoaded: true,
|
||||
value: [result],
|
||||
isLoading: false,
|
||||
filter: filter,
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Updates a document. If [shouldReload] is false, the updated document will
|
||||
/// replace the currently loaded one, otherwise all documents will be reloaded.
|
||||
///
|
||||
Future<void> update(DocumentModel document) async {
|
||||
final updatedDocument = await api.update(document);
|
||||
notifier.notifyUpdated(updatedDocument);
|
||||
// replace(updatedDocument);
|
||||
}
|
||||
|
||||
///
|
||||
/// Deletes a document and removes it from the currently loaded state.
|
||||
///
|
||||
Future<void> delete(DocumentModel document) async {
|
||||
emit(state.copyWithPaged(isLoading: true));
|
||||
try {
|
||||
await api.delete(document);
|
||||
notifier.notifyDeleted(document);
|
||||
// remove(document); // Removing deleted now works with the change notifier.
|
||||
} finally {
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Removes [document] from the currently loaded state.
|
||||
/// Does not delete it from the server!
|
||||
///
|
||||
void remove(DocumentModel document) {
|
||||
final index = state.value.indexWhere(
|
||||
(page) => page.results.any((element) => element.id == document.id),
|
||||
);
|
||||
if (index != -1) {
|
||||
final foundPage = state.value[index];
|
||||
final replacementPage = foundPage.copyWith(
|
||||
results: foundPage.results
|
||||
..removeWhere((element) => element.id == document.id),
|
||||
);
|
||||
final newCount = foundPage.count - 1;
|
||||
emit(
|
||||
state.copyWithPaged(
|
||||
value: state.value
|
||||
.mapIndexed(
|
||||
(currIndex, element) =>
|
||||
(currIndex == index ? replacementPage : element)
|
||||
.copyWith(count: newCount),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Replaces the document with the same id as [document] from the currently
|
||||
/// loaded state if the document's properties still match the given filter criteria, otherwise removes it.
|
||||
///
|
||||
Future<void> replace(DocumentModel document) async {
|
||||
final matchesFilterCriteria = state.filter.matches(document);
|
||||
if (!matchesFilterCriteria) {
|
||||
return remove(document);
|
||||
}
|
||||
final pageIndex = state.value.indexWhere(
|
||||
(page) => page.results.any((element) => element.id == document.id),
|
||||
);
|
||||
if (pageIndex != -1) {
|
||||
final foundPage = state.value[pageIndex];
|
||||
final replacementPage = foundPage.copyWith(
|
||||
results: foundPage.results
|
||||
.map((doc) => doc.id == document.id ? document : doc)
|
||||
.toList(),
|
||||
);
|
||||
final newState = state.copyWithPaged(
|
||||
value: state.value
|
||||
.mapIndexed((currIndex, element) =>
|
||||
currIndex == pageIndex ? replacementPage : element)
|
||||
.toList(),
|
||||
);
|
||||
emit(newState);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
notifier.unsubscribe(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
///
|
||||
/// Base state for all blocs/cubits using a paged view of documents.
|
||||
/// [T] is the return type of the API call.
|
||||
///
|
||||
abstract class DocumentPagingState extends Equatable {
|
||||
final bool hasLoaded;
|
||||
final bool isLoading;
|
||||
final List<PagedSearchResult<DocumentModel>> value;
|
||||
final DocumentFilter filter;
|
||||
|
||||
const DocumentPagingState({
|
||||
this.value = const [],
|
||||
this.hasLoaded = false,
|
||||
this.isLoading = false,
|
||||
this.filter = const DocumentFilter(),
|
||||
});
|
||||
|
||||
List<DocumentModel> get documents {
|
||||
return value.fold(
|
||||
[],
|
||||
(previousValue, element) => [
|
||||
...previousValue,
|
||||
...element.results,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int get currentPageNumber {
|
||||
assert(value.isNotEmpty);
|
||||
return value.last.pageKey;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Return type has to be dynamic
|
||||
dynamic copyWithPaged({
|
||||
bool? hasLoaded,
|
||||
bool? isLoading,
|
||||
List<PagedSearchResult<DocumentModel>>? value,
|
||||
DocumentFilter? filter,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
filter,
|
||||
value,
|
||||
hasLoaded,
|
||||
isLoading,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user