Changed saved views handling, changed repository structure with automatic persistence.

This commit is contained in:
Anton Stubenbord
2023-01-08 00:01:04 +01:00
parent 23bcb355b1
commit 3c6c4e63d7
74 changed files with 1374 additions and 863 deletions

View File

@@ -1,15 +1,22 @@
import 'dart:async';
import 'dart:developer';
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';
class DocumentsCubit extends HydratedCubit<DocumentsState> {
class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
final PaperlessDocumentsApi _api;
final SavedViewRepository _savedViewRepository;
DocumentsCubit(this._api) : super(const DocumentsState());
DocumentsCubit(this._api, this._savedViewRepository)
: super(const DocumentsState()) {
hydrate();
}
Future<void> bulkRemove(List<DocumentModel> documents) async {
log("[DocumentsCubit] bulkRemove");
await _api.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)),
);
@@ -21,6 +28,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
Iterable<int> addTags = const [],
Iterable<int> removeTags = const [],
}) async {
log("[DocumentsCubit] bulkEditTags");
await _api.bulkAction(BulkModifyTagsAction(
documents.map((doc) => doc.id),
addTags: addTags,
@@ -33,6 +41,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
DocumentModel document, [
bool updateRemote = true,
]) async {
log("[DocumentsCubit] update");
if (updateRemote) {
await _api.update(document);
}
@@ -40,6 +49,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
}
Future<void> load() async {
log("[DocumentsCubit] load");
emit(state.copyWith(isLoading: true));
try {
final result = await _api.find(state.filter);
@@ -48,33 +58,23 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
hasLoaded: true,
value: [...state.value, result],
));
} catch (err) {
} finally {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
Future<void> reload() async {
log("[DocumentsCubit] reload");
emit(state.copyWith(isLoading: true));
try {
if (state.currentPageNumber >= 5) {
return _bulkReloadDocuments();
}
var newPages = <PagedSearchResult<DocumentModel>>[];
for (final page in state.value) {
final result =
await _api.find(state.filter.copyWith(page: page.pageKey));
newPages.add(result);
}
emit(DocumentsState(
final result = await _api.find(state.filter.copyWith(page: 1));
emit(state.copyWith(
hasLoaded: true,
value: newPages,
filter: state.filter,
value: [result],
isLoading: false,
));
} catch (err) {
} finally {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
@@ -93,13 +93,13 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
filter: state.filter,
isLoading: false,
));
} catch (err) {
} finally {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
Future<void> loadMore() async {
log("[DocumentsCubit] loadMore");
if (state.isLastPageLoaded) {
return;
}
@@ -115,21 +115,22 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
isLoading: false,
),
);
} catch (err) {
} finally {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
///
/// Update filter state and automatically reload documents. Always resets page to 1.
/// Use [DocumentsCubit.loadMore] to load more data.
/// 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.find(filter.copyWith(page: 1));
emit(
DocumentsState(
filter: filter,
@@ -138,13 +139,13 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
isLoading: false,
),
);
} catch (err) {
} finally {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
Future<void> resetFilter() {
log("[DocumentsCubit] resetFilter");
final filter = DocumentFilter.initial.copyWith(
sortField: state.filter.sortField,
sortOrder: state.filter.sortOrder,
@@ -161,6 +162,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
updateFilter(filter: transformFn(state.filter));
void toggleDocumentSelection(DocumentModel model) {
log("[DocumentsCubit] toggleSelection");
if (state.selection.contains(model)) {
emit(
state.copyWith(
@@ -177,16 +179,44 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> {
}
void resetSelection() {
log("[DocumentsCubit] resetSelection");
emit(state.copyWith(selection: []));
}
void reset() {
log("[DocumentsCubit] reset");
emit(const DocumentsState());
}
Future<void> selectView(int id) async {
emit(state.copyWith(isLoading: true));
try {
final filter =
_savedViewRepository.current?.values[id]?.toDocumentFilter();
if (filter == null) {
return;
}
final results = await _api.find(filter.copyWith(page: 1));
emit(
DocumentsState(
filter: filter,
hasLoaded: true,
isLoading: false,
selectedSavedViewId: id,
value: [results],
),
);
} finally {
emit(state.copyWith(isLoading: false));
}
}
void unselectView() {
emit(state.copyWith(selectedSavedViewId: null));
}
@override
DocumentsState? fromJson(Map<String, dynamic> json) {
log(json['filter'].toString());
return DocumentsState.fromJson(json);
}

View File

@@ -4,12 +4,12 @@ import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
@JsonSerializable()
class DocumentsState extends Equatable {
final bool isLoading;
final bool hasLoaded;
final DocumentFilter filter;
final List<PagedSearchResult<DocumentModel>> value;
final int? selectedSavedViewId;
@JsonKey(ignore: true)
final List<DocumentModel> selection;
@@ -20,6 +20,7 @@ class DocumentsState extends Equatable {
this.value = const [],
this.filter = const DocumentFilter(),
this.selection = const [],
this.selectedSavedViewId,
});
int get currentPageNumber {
@@ -69,6 +70,7 @@ class DocumentsState extends Equatable {
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
List<DocumentModel>? selection,
int? selectedSavedViewId,
}) {
return DocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded,
@@ -76,17 +78,26 @@ class DocumentsState extends Equatable {
value: value ?? this.value,
filter: filter ?? this.filter,
selection: selection ?? this.selection,
selectedSavedViewId: selectedSavedViewId ?? this.selectedSavedViewId,
);
}
@override
List<Object?> get props => [hasLoaded, filter, value, selection, isLoading];
List<Object?> get props => [
hasLoaded,
filter,
value,
selection,
isLoading,
selectedSavedViewId,
];
Map<String, dynamic> toJson() {
final json = {
'hasLoaded': hasLoaded,
'isLoading': isLoading,
'filter': filter.toJson(),
'selectedSavedViewId': selectedSavedViewId,
'value':
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
};
@@ -97,9 +108,10 @@ class DocumentsState extends Equatable {
return DocumentsState(
hasLoaded: json['hasLoaded'],
isLoading: json['isLoading'],
selectedSavedViewId: json['selectedSavedViewId'],
value: (json['value'] as List<dynamic>)
.map((e) =>
PagedSearchResult.fromJson(e, DocumentModelJsonConverter()))
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
.toList(),
filter: DocumentFilter.fromJson(json['filter']),
);

View File

@@ -7,6 +7,9 @@ import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
@@ -80,7 +83,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
state.document.storagePath, state.storagePaths)
.padded(),
TagFormField(
initialValue: IdsTagsQuery.included(state.document.tags),
initialValue:
IdsTagsQuery.included(state.document.tags.toList()),
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
@@ -100,7 +104,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read<LabelRepository<StoragePath>>(),
create: (context) => context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
child: AddStoragePathPage(initalValue: initialValue),
),
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
@@ -117,7 +122,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read<LabelRepository<Correspondent>>(),
create: (context) => context.read<
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
child: AddCorrespondentPage(initialName: initialValue),
),
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
@@ -134,7 +140,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
create: (context) => context.read<LabelRepository<DocumentType>>(),
create: (context) => context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
child: AddDocumentTypePage(
initialName: currentInput,
),

View File

@@ -1,8 +1,11 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.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';
@@ -23,6 +26,17 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/util.dart';
import 'package:collection/collection.dart';
class DocumentFilterIntent {
final DocumentFilter? filter;
final bool shouldReset;
DocumentFilterIntent({
this.filter,
this.shouldReset = false,
});
}
class DocumentsPage extends StatefulWidget {
const DocumentsPage({Key? key}) : super(key: key);
@@ -35,13 +49,13 @@ class _DocumentsPageState extends State<DocumentsPage> {
final _pagingController = PagingController<int, DocumentModel>(
firstPageKey: 1,
);
final _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@override
void initState() {
super.initState();
try {
context.read<DocumentsCubit>().load();
context.read<DocumentsCubit>().reload();
context.read<SavedViewCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
@@ -62,7 +76,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
current == ConnectivityState.connected,
listener: (context, state) {
try {
context.read<DocumentsCubit>().load();
context.read<DocumentsCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
@@ -101,7 +115,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
void _openDocumentFilter() async {
final draggableSheetController = DraggableScrollableController();
final filter = await showModalBottomSheet<DocumentFilter>(
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
useSafeArea: true,
context: context,
shape: const RoundedRectangleBorder(
@@ -130,9 +144,23 @@ class _DocumentsPageState extends State<DocumentsPage> {
),
),
);
if (filter != null) {
context.read<DocumentsCubit>().updateFilter(filter: filter);
context.read<SavedViewCubit>().resetSelection();
if (filterIntent != null) {
try {
if (filterIntent.shouldReset) {
await context.read<DocumentsCubit>().resetFilter();
context.read<DocumentsCubit>().unselectView();
} else {
if (filterIntent.filter !=
context.read<DocumentsCubit>().state.filter) {
context.read<DocumentsCubit>().unselectView();
}
await context
.read<DocumentsCubit>()
.updateFilter(filter: filterIntent.filter!);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
@@ -141,6 +169,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) => !const ListEquality()
.equals(previous.documents, current.documents),
builder: (context, state) {
// Some ugly tricks to make it work with bloc, update pageController
_pagingController.value = PagingState(
@@ -184,59 +214,34 @@ class _DocumentsPageState extends State<DocumentsPage> {
state: state,
onReset: () {
context.read<DocumentsCubit>().resetFilter();
context.read<SavedViewCubit>().resetSelection();
context.read<DocumentsCubit>().unselectView();
},
),
);
}
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _onRefresh,
notificationPredicate: (_) => isConnected,
child: CustomScrollView(
slivers: [
BlocListener<SavedViewCubit, SavedViewState>(
listenWhen: (previous, current) =>
previous.selectedSavedViewId !=
current.selectedSavedViewId,
listener: (context, state) {
try {
if (state.selectedSavedViewId == null) {
context.read<DocumentsCubit>().resetFilter();
} else {
final newFilter = state
.value[state.selectedSavedViewId]
?.toDocumentFilter();
if (newFilter != null) {
context
.read<DocumentsCubit>()
.updateFilter(filter: newFilter);
}
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
child: DocumentsPageAppBar(
isOffline:
connectivityState != ConnectivityState.connected,
actions: [
const SortDocumentsButton(),
IconButton(
icon: Icon(
settings.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view,
),
onPressed: () => context
.read<ApplicationSettingsCubit>()
.setViewType(
settings.preferredViewType.toggle(),
),
DocumentsPageAppBar(
isOffline: connectivityState != ConnectivityState.connected,
actions: [
const SortDocumentsButton(),
IconButton(
icon: Icon(
settings.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view,
),
],
),
onPressed: () => context
.read<ApplicationSettingsCubit>()
.setViewType(
settings.preferredViewType.toggle(),
),
),
],
),
child,
],
@@ -249,10 +254,13 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
Future<void> _openDetails(DocumentModel document) async {
await Navigator.of(context).push<DocumentModel?>(
final potentiallyUpdatedModel =
await Navigator.of(context).push<DocumentModel?>(
_buildDetailsPageRoute(document),
);
context.read<DocumentsCubit>().reload();
if (potentiallyUpdatedModel != document) {
context.read<DocumentsCubit>().reload();
}
}
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
@@ -371,9 +379,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
Future<void> _onRefresh() async {
try {
await context.read<DocumentsCubit>().updateCurrentFilter(
(filter) => filter.copyWith(page: 1),
);
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<DocumentsCubit>().reload();
await context.read<SavedViewCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);

View File

@@ -6,6 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
@@ -228,10 +229,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
FocusScope.of(context).unfocus();
Navigator.pop(
context,
DocumentFilter.initial.copyWith(
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
),
DocumentFilterIntent(shouldReset: true),
);
}
@@ -293,7 +291,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
if (_formKey.currentState?.validate() ?? false) {
DocumentFilter newFilter = _assembleFilter();
FocusScope.of(context).unfocus();
Navigator.pop(context, newFilter);
Navigator.pop(context, DocumentFilterIntent(filter: newFilter));
}
}

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.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/view/widgets/search/sort_field_selection_bottom_sheet.dart';
@@ -37,12 +39,16 @@ class SortDocumentsButton extends StatelessWidget {
providers: [
BlocProvider(
create: (context) => LabelCubit<DocumentType>(
context.read<LabelRepository<DocumentType>>(),
context.read<
LabelRepository<DocumentType,
DocumentTypeRepositoryState>>(),
),
),
BlocProvider(
create: (context) => LabelCubit<Correspondent>(
context.read<LabelRepository<Correspondent>>(),
context.read<
LabelRepository<Correspondent,
CorrespondentRepositoryState>>(),
),
),
],