WIP - More decoupling of data layer from ui layer

This commit is contained in:
Anton Stubenbord
2022-12-09 00:54:39 +01:00
parent 75fa2f7713
commit c9694fa8d0
87 changed files with 2508 additions and 1879 deletions

View File

@@ -4,8 +4,7 @@ import 'package:paperless_api/paperless_api.dart';
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/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class DocumentsEmptyState extends StatelessWidget {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.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/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';

View File

@@ -1,8 +1,10 @@
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/provider/label_repositories_provider.dart';
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/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
@@ -34,21 +36,23 @@ class DocumentListView extends StatelessWidget {
builderDelegate: PagedChildBuilderDelegate(
animateTransitions: true,
itemBuilder: (context, document, index) {
return DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
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,
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
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

@@ -4,15 +4,12 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.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/view/widgets/search/query_type_form_field.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.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/storage_path/bloc/storage_path_cubit.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/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/util.dart';
@@ -24,10 +21,15 @@ class DocumentFilterPanel extends StatefulWidget {
final PanelController panelController;
final ScrollController scrollController;
final DocumentFilter initialFilter;
final void Function(DocumentFilter filter) onFilterChanged;
const DocumentFilterPanel({
Key? key,
required this.panelController,
required this.scrollController,
required this.onFilterChanged,
required this.initialFilter,
}) : super(key: key);
@override
@@ -63,93 +65,84 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: BlocConsumer<DocumentsCubit, DocumentsState>(
listener: (context, state) {
// Set initial values, otherwise they would not automatically update.
_patchFromFilter(state.filter);
},
builder: (context, state) {
return FormBuilder(
key: _formKey,
child: Column(
child: FormBuilder(
key: _formKey,
child: Column(
children: [
Stack(
alignment: Alignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
_buildDragLine(),
Align(
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label: Text(
S.of(context).documentsFilterPageResetFilterLabel),
onPressed: () => _resetFilter(context),
),
),
],
),
const SizedBox(
height: 8.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).documentsFilterPageTitle,
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: _onApplyFilter,
child: Text(
S.of(context).documentsFilterPageApplyFilterLabel),
),
],
).padded(),
const SizedBox(
height: 16.0,
),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: ListView(
controller: widget.scrollController,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentsFilterPageSearchLabel),
).padded(const EdgeInsets.only(left: 8.0)),
_buildQueryFormField(state),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentsFilterPageAdvancedLabel),
).padded(const EdgeInsets.only(left: 8.0, top: 8.0)),
_buildCreatedDateRangePickerFormField(state).padded(),
_buildAddedDateRangePickerFormField(state).padded(),
_buildCorrespondentFormField(state).padded(),
_buildDocumentTypeFormField(state).padded(),
_buildStoragePathFormField(state).padded(),
TagFormField(
name: DocumentModel.tagsKey,
initialValue: state.filter.tags,
allowCreation: false,
).padded(),
// Required in order for the storage path field to be visible when typing
const SizedBox(
height: 150,
),
],
).padded(),
_buildDragLine(),
Align(
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label:
Text(S.of(context).documentsFilterPageResetFilterLabel),
onPressed: () => _resetFilter(context),
),
),
],
),
);
},
const SizedBox(
height: 8.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).documentsFilterPageTitle,
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: _onApplyFilter,
child:
Text(S.of(context).documentsFilterPageApplyFilterLabel),
),
],
).padded(),
const SizedBox(
height: 16.0,
),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: ListView(
controller: widget.scrollController,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(S.of(context).documentsFilterPageSearchLabel),
).padded(const EdgeInsets.only(left: 8.0)),
_buildQueryFormField(),
Align(
alignment: Alignment.centerLeft,
child:
Text(S.of(context).documentsFilterPageAdvancedLabel),
).padded(const EdgeInsets.only(left: 8.0, top: 8.0)),
_buildCreatedDateRangePickerFormField().padded(),
_buildAddedDateRangePickerFormField().padded(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
TagFormField(
name: DocumentModel.tagsKey,
initialValue: widget.initialFilter.tags,
allowCreation: false,
).padded(),
// Required in order for the storage path field to be visible when typing
const SizedBox(
height: 150,
),
],
).padded(),
),
),
],
),
),
);
}
@@ -163,15 +156,16 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
}
}
Widget _buildDocumentTypeFormField(DocumentsState docState) {
return BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
//TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField!
Widget _buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
formBuilderState: _formKey.currentState,
name: fkDocumentType,
state: state.labels,
label: S.of(context).documentDocumentTypePropertyLabel,
initialValue: docState.filter.documentType,
initialValue: widget.initialFilter.documentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined),
@@ -180,15 +174,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildCorrespondentFormField(DocumentsState docState) {
return BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
Widget _buildCorrespondentFormField() {
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
formBuilderState: _formKey.currentState,
name: fkCorrespondent,
state: state.labels,
label: S.of(context).documentCorrespondentPropertyLabel,
initialValue: docState.filter.correspondent,
initialValue: widget.initialFilter.correspondent,
queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outline),
@@ -197,15 +191,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildStoragePathFormField(DocumentsState docState) {
return BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
Widget _buildStoragePathFormField() {
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>(
formBuilderState: _formKey.currentState,
name: fkStoragePath,
state: state.labels,
label: S.of(context).documentStoragePathPropertyLabel,
initialValue: docState.filter.storagePath,
initialValue: widget.initialFilter.storagePath,
queryParameterIdBuilder: StoragePathQuery.fromId,
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
prefixIcon: const Icon(Icons.folder_outlined),
@@ -214,7 +208,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildQueryFormField(DocumentsState state) {
Widget _buildQueryFormField() {
final queryType =
_formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
QueryType.titleAndContent;
@@ -239,16 +233,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
prefixIcon: const Icon(Icons.search_outlined),
labelText: label,
suffixIcon: QueryTypeFormField(
initialValue: state.filter.queryType,
initialValue: widget.initialFilter.queryType,
afterSelected: (queryType) => setState(() {}),
),
),
initialValue: state.filter.queryText,
initialValue: widget.initialFilter.queryText,
).padded();
}
Widget _buildDateRangePickerHelper(
DocumentsState state, String formFieldKey) {
Widget _buildDateRangePickerHelper(String formFieldKey) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
@@ -328,13 +321,13 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildCreatedDateRangePickerFormField(DocumentsState state) {
Widget _buildCreatedDateRangePickerFormField() {
return Column(
children: [
FormBuilderDateRangePicker(
initialValue: _dateTimeRangeOfNullable(
state.filter.createdDateAfter,
state.filter.createdDateBefore,
widget.initialFilter.createdDateAfter,
widget.initialFilter.createdDateBefore,
),
// Workaround for theme data not being correctly passed to daterangepicker, see
// https://github.com/flutter/flutter/issues/87580
@@ -371,18 +364,18 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
),
),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkCreatedAt),
_buildDateRangePickerHelper(fkCreatedAt),
],
);
}
Widget _buildAddedDateRangePickerFormField(DocumentsState state) {
Widget _buildAddedDateRangePickerFormField() {
return Column(
children: [
FormBuilderDateRangePicker(
initialValue: _dateTimeRangeOfNullable(
state.filter.addedDateAfter,
state.filter.addedDateBefore,
widget.initialFilter.addedDateAfter,
widget.initialFilter.addedDateBefore,
),
// Workaround for theme data not being correctly passed to daterangepicker, see
// https://github.com/flutter/flutter/issues/87580
@@ -419,7 +412,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
),
),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkAddedAt),
_buildDateRangePickerHelper(fkAddedAt),
],
);
}

View File

@@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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/documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget {
const SortFieldSelectionBottomSheet({super.key});
final SortOrder initialSortOrder;
final SortField initialSortField;
final Future Function(SortField field, SortOrder order) onSubmit;
const SortFieldSelectionBottomSheet({
super.key,
required this.initialSortOrder,
required this.initialSortField,
required this.onSubmit,
});
@override
State<SortFieldSelectionBottomSheet> createState() =>
@@ -17,81 +23,60 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
class _SortFieldSelectionBottomSheetState
extends State<SortFieldSelectionBottomSheet> {
SortField? _selectedFieldLoading;
SortOrder? _selectedOrderLoading;
late SortField _currentSortField;
late SortOrder _currentSortOrder;
@override
void initState() {
super.initState();
_currentSortField = widget.initialSortField;
_currentSortOrder = widget.initialSortOrder;
}
@override
Widget build(BuildContext context) {
return ClipRRect(
child: BlocBuilder<DocumentsCubit, DocumentsState>(
bloc: getIt<DocumentsCubit>(),
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).documentsPageOrderByLabel,
style: Theme.of(context).textTheme.caption,
textAlign: TextAlign.start,
).padded(
const EdgeInsets.symmetric(horizontal: 16, vertical: 16)),
Column(
children: SortField.values
.map(
(e) => _buildSortOption(
e,
state.filter.sortOrder,
state.filter.sortField == e,
_selectedFieldLoading == e,
),
)
.toList(),
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
TextButton(
child: Text(S.of(context).documentsFilterPageApplyFilterLabel),
onPressed: () => widget.onSubmit(
_currentSortField,
_currentSortOrder,
),
),
],
);
},
),
Column(
children: SortField.values.map(_buildSortOption).toList(),
),
],
),
);
}
Widget _buildSortOption(
SortField field,
SortOrder order,
bool isCurrentlySelected,
bool isNextSelected,
) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text(
_localizedSortField(field),
),
trailing: isNextSelected
? (_buildOrderIcon(_selectedOrderLoading!))
: (_selectedOrderLoading == null && isCurrentlySelected
? _buildOrderIcon(order)
: null),
onTap: () async {
setState(() {
_selectedFieldLoading = field;
_selectedOrderLoading =
isCurrentlySelected ? order.toggle() : SortOrder.descending;
});
BlocProvider.of<DocumentsCubit>(context)
.updateCurrentFilter((filter) => filter.copyWith(
sortOrder: isCurrentlySelected
? order.toggle()
: SortOrder.descending,
sortField: field,
))
.whenComplete(() {
if (mounted) {
setState(() {
_selectedFieldLoading = null;
_selectedOrderLoading = null;
});
}
});
},
trailing: _currentSortField == field
? _buildOrderIcon(_currentSortOrder)
: null,
);
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class BulkDeleteConfirmationDialog extends StatelessWidget {

View File

@@ -1,9 +1,10 @@
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/saved_view_repository.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/selection/bulk_delete_confirmation_dialog.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
@@ -79,7 +80,6 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
//TODO: replace with sorting stuff...
SavedViewSelectionWidget(height: 48, enabled: enabled),
],
),

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
@@ -33,11 +32,19 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
topRight: Radius.circular(16),
),
),
builder: (context) => BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: const FractionallySizedBox(
heightFactor: .6,
child: SortFieldSelectionBottomSheet(),
builder: (context) => FractionallySizedBox(
heightFactor: .6,
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return SortFieldSelectionBottomSheet(
initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) =>
BlocProvider.of<DocumentsCubit>(context).updateCurrentFilter(
(filter) => filter.copyWith(sortField: field, sortOrder: order),
),
);
},
),
),
);