mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 07:15:47 -06:00
Improved search, changed saved view display
This commit is contained in:
0
lib/features/app_drawer/view/app_drawer.dart
Normal file
0
lib/features/app_drawer/view/app_drawer.dart
Normal file
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
|
||||||
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
|
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
@@ -58,11 +60,6 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
|
|||||||
);
|
);
|
||||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (!state.hasLoaded) {
|
|
||||||
return const DocumentsListLoadingWidget(
|
|
||||||
beforeWidgets: [earlyPreviewHintCard],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (state.documents.isEmpty) {
|
if (state.documents.isEmpty) {
|
||||||
return DocumentsEmptyState(
|
return DocumentsEmptyState(
|
||||||
state: state,
|
state: state,
|
||||||
@@ -74,20 +71,36 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return CustomScrollView(
|
|
||||||
controller: _scrollController,
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
slivers: [
|
builder: (context, connectivity) {
|
||||||
const SliverToBoxAdapter(child: earlyPreviewHintCard),
|
return CustomScrollView(
|
||||||
SliverList(
|
controller: _scrollController,
|
||||||
delegate: SliverChildBuilderDelegate(
|
slivers: [
|
||||||
childCount: state.documents.length,
|
const SliverToBoxAdapter(child: earlyPreviewHintCard),
|
||||||
(context, index) => DocumentListItem(
|
SliverAdaptiveDocumentsView(
|
||||||
document: state.documents[index],
|
documents: state.documents,
|
||||||
enableHeroAnimation: false,
|
hasInternetConnection: connectivity.isConnected,
|
||||||
|
isLabelClickable: false,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
|
||||||
),
|
),
|
||||||
),
|
SliverList(
|
||||||
),
|
delegate: SliverChildBuilderDelegate(
|
||||||
],
|
childCount: state.documents.length,
|
||||||
|
(context, index) => DocumentListItem(
|
||||||
|
document: state.documents[index],
|
||||||
|
enableHeroAnimation: false,
|
||||||
|
isLabelClickable: false,
|
||||||
|
isSelected: false,
|
||||||
|
isSelectionActive: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,38 +66,11 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
emit(const DocumentsState());
|
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.findAll(filter.copyWith(page: 1));
|
|
||||||
emit(
|
|
||||||
DocumentsState(
|
|
||||||
filter: filter,
|
|
||||||
hasLoaded: true,
|
|
||||||
isLoading: false,
|
|
||||||
selectedSavedViewId: id,
|
|
||||||
value: [results],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
emit(state.copyWith(isLoading: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Iterable<String>> autocomplete(String query) async {
|
Future<Iterable<String>> autocomplete(String query) async {
|
||||||
final res = await api.autocomplete(query);
|
final res = await api.autocomplete(query);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void unselectView() {
|
|
||||||
emit(state.copyWith(selectedSavedViewId: () => null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DocumentsState? fromJson(Map<String, dynamic> json) {
|
DocumentsState? fromJson(Map<String, dynamic> json) {
|
||||||
return DocumentsState.fromJson(json);
|
return DocumentsState.fromJson(json);
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||||
|
|
||||||
class DocumentsState extends PagedDocumentsState {
|
class DocumentsState extends PagedDocumentsState {
|
||||||
final int? selectedSavedViewId;
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
final List<DocumentModel> selection;
|
final List<DocumentModel> selection;
|
||||||
|
|
||||||
const DocumentsState({
|
const DocumentsState({
|
||||||
this.selection = const [],
|
this.selection = const [],
|
||||||
this.selectedSavedViewId,
|
|
||||||
super.value = const [],
|
super.value = const [],
|
||||||
super.filter = const DocumentFilter(),
|
super.filter = const DocumentFilter(),
|
||||||
super.hasLoaded = false,
|
super.hasLoaded = false,
|
||||||
@@ -25,7 +22,6 @@ class DocumentsState extends PagedDocumentsState {
|
|||||||
List<PagedSearchResult<DocumentModel>>? value,
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
DocumentFilter? filter,
|
DocumentFilter? filter,
|
||||||
List<DocumentModel>? selection,
|
List<DocumentModel>? selection,
|
||||||
int? Function()? selectedSavedViewId,
|
|
||||||
}) {
|
}) {
|
||||||
return DocumentsState(
|
return DocumentsState(
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
@@ -33,9 +29,6 @@ class DocumentsState extends PagedDocumentsState {
|
|||||||
value: value ?? this.value,
|
value: value ?? this.value,
|
||||||
filter: filter ?? this.filter,
|
filter: filter ?? this.filter,
|
||||||
selection: selection ?? this.selection,
|
selection: selection ?? this.selection,
|
||||||
selectedSavedViewId: selectedSavedViewId != null
|
|
||||||
? selectedSavedViewId.call()
|
|
||||||
: this.selectedSavedViewId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +39,6 @@ class DocumentsState extends PagedDocumentsState {
|
|||||||
value,
|
value,
|
||||||
selection,
|
selection,
|
||||||
isLoading,
|
isLoading,
|
||||||
selectedSavedViewId,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -54,7 +46,6 @@ class DocumentsState extends PagedDocumentsState {
|
|||||||
'hasLoaded': hasLoaded,
|
'hasLoaded': hasLoaded,
|
||||||
'isLoading': isLoading,
|
'isLoading': isLoading,
|
||||||
'filter': filter.toJson(),
|
'filter': filter.toJson(),
|
||||||
'selectedSavedViewId': selectedSavedViewId,
|
|
||||||
'value':
|
'value':
|
||||||
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
|
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
|
||||||
};
|
};
|
||||||
@@ -65,7 +56,6 @@ class DocumentsState extends PagedDocumentsState {
|
|||||||
return DocumentsState(
|
return DocumentsState(
|
||||||
hasLoaded: json['hasLoaded'],
|
hasLoaded: json['hasLoaded'],
|
||||||
isLoading: json['isLoading'],
|
isLoading: json['isLoading'],
|
||||||
selectedSavedViewId: json['selectedSavedViewId'],
|
|
||||||
value: (json['value'] as List<dynamic>)
|
value: (json['value'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) =>
|
||||||
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
|
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:badges/badges.dart' as b;
|
import 'package:badges/badges.dart' as b;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -5,35 +7,26 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||||
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/app_options_popup_menu.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.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';
|
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
||||||
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_search/document_search_delegate.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.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/bloc/documents_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.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/features/saved_view/view/add_saved_view_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
||||||
import 'package:paperless_mobile/features/search/view/document_search_page.dart';
|
import 'package:paperless_mobile/features/search/view/document_search_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart';
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class DocumentFilterIntent {
|
class DocumentFilterIntent {
|
||||||
@@ -46,6 +39,7 @@ class DocumentFilterIntent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Refactor this
|
||||||
class DocumentsPage extends StatefulWidget {
|
class DocumentsPage extends StatefulWidget {
|
||||||
const DocumentsPage({Key? key}) : super(key: key);
|
const DocumentsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -53,56 +47,38 @@ class DocumentsPage extends StatefulWidget {
|
|||||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentsPageState extends State<DocumentsPage> {
|
class _DocumentsPageState extends State<DocumentsPage>
|
||||||
final ScrollController _scrollController = ScrollController();
|
with SingleTickerProviderStateMixin {
|
||||||
double _offset = 0;
|
late final TabController _tabController;
|
||||||
double _last = 0;
|
|
||||||
|
|
||||||
static const double _savedViewWidgetHeight = 80 + 16;
|
int _currentTab = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_tabController = TabController(
|
||||||
|
length: 2,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: 0,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
context.read<DocumentsCubit>().reload();
|
context.read<DocumentsCubit>().reload();
|
||||||
context.read<SavedViewCubit>().reload();
|
context.read<SavedViewCubit>().reload();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
_scrollController
|
_tabController.addListener(_listenForTabChanges);
|
||||||
..addListener(_listenForScrollChanges)
|
|
||||||
..addListener(_listenForLoadNewData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenForLoadNewData() async {
|
void _listenForTabChanges() {
|
||||||
final currState = context.read<DocumentsCubit>().state;
|
setState(() {
|
||||||
if (_scrollController.offset >=
|
_currentTab = _tabController.index;
|
||||||
_scrollController.position.maxScrollExtent * 0.75 &&
|
});
|
||||||
!currState.isLoading &&
|
|
||||||
!currState.isLastPageLoaded) {
|
|
||||||
try {
|
|
||||||
await context.read<DocumentsCubit>().loadMore();
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenForScrollChanges() {
|
|
||||||
final current = _scrollController.offset;
|
|
||||||
_offset += _last - current;
|
|
||||||
|
|
||||||
if (_offset <= -_savedViewWidgetHeight) _offset = -_savedViewWidgetHeight;
|
|
||||||
if (_offset >= 0) _offset = 0;
|
|
||||||
_last = current;
|
|
||||||
if (_offset <= 0 && _offset >= -_savedViewWidgetHeight) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,149 +116,204 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
},
|
},
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// appBar: PreferredSize(
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
// preferredSize: const Size.fromHeight(
|
builder: (context, state) {
|
||||||
// kToolbarHeight,
|
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||||
// ),
|
return b.Badge(
|
||||||
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
// builder: (context, state) {
|
showBadge: appliedFiltersCount > 0,
|
||||||
// if (state.selection.isEmpty) {
|
badgeContent: Text(
|
||||||
// return DocumentSearchBar();
|
'$appliedFiltersCount',
|
||||||
// // return AppBar(
|
style: const TextStyle(
|
||||||
// // title: Text(S.of(context).documentsPageTitle +
|
color: Colors.white,
|
||||||
// // " (${formatMaxCount(state.documents.length)})"),
|
),
|
||||||
// // actions: [
|
),
|
||||||
// // IconButton(
|
animationType: b.BadgeAnimationType.fade,
|
||||||
// // icon: const Icon(Icons.search),
|
badgeColor: Colors.red,
|
||||||
// // onPressed: () {
|
child: _currentTab == 0
|
||||||
// // showMaterial3Search(
|
? FloatingActionButton(
|
||||||
// // context: context,
|
child: const Icon(Icons.filter_alt_outlined),
|
||||||
// // delegate: DocumentSearchDelegate(
|
onPressed: _openDocumentFilter,
|
||||||
// // DocumentSearchCubit(context.read()),
|
)
|
||||||
// // searchFieldStyle:
|
: FloatingActionButton(
|
||||||
// // Theme.of(context).textTheme.bodyLarge,
|
child: const Icon(Icons.add),
|
||||||
// // hintText: "Search documents", //TODO: INTL
|
onPressed: () => _onCreateSavedView(state.filter),
|
||||||
// // ),
|
),
|
||||||
// // );
|
);
|
||||||
// // },
|
},
|
||||||
// // ),
|
),
|
||||||
// // const SortDocumentsButton(),
|
|
||||||
// // const AppOptionsPopupMenu(
|
|
||||||
// // displayedActions: [
|
|
||||||
// // AppPopupMenuEntries.documentsSelectListView,
|
|
||||||
// // AppPopupMenuEntries.documentsSelectGridView,
|
|
||||||
// // AppPopupMenuEntries.divider,
|
|
||||||
// // AppPopupMenuEntries.openAboutThisAppDialog,
|
|
||||||
// // AppPopupMenuEntries.reportBug,
|
|
||||||
// // AppPopupMenuEntries.openSettings,
|
|
||||||
// // ],
|
|
||||||
// // ),
|
|
||||||
// // ],
|
|
||||||
// // );
|
|
||||||
// } else {
|
|
||||||
// return AppBar(
|
|
||||||
// leading: IconButton(
|
|
||||||
// icon: const Icon(Icons.close),
|
|
||||||
// onPressed: () =>
|
|
||||||
// context.read<DocumentsCubit>().resetSelection(),
|
|
||||||
// ),
|
|
||||||
// title: Text(
|
|
||||||
// '${state.selection.length} ${S.of(context).documentsSelectedText}'),
|
|
||||||
// actions: [
|
|
||||||
// IconButton(
|
|
||||||
// icon: const Icon(Icons.delete),
|
|
||||||
// onPressed: () => _onDelete(context, state),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
// builder: (context, state) {
|
|
||||||
// final appliedFiltersCount = state.filter.appliedFiltersCount;
|
|
||||||
// return b.Badge(
|
|
||||||
// position: b.BadgePosition.topEnd(top: -12, end: -6),
|
|
||||||
// showBadge: appliedFiltersCount > 0,
|
|
||||||
// badgeContent: Text(
|
|
||||||
// '$appliedFiltersCount',
|
|
||||||
// style: const TextStyle(
|
|
||||||
// color: Colors.white,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// animationType: b.BadgeAnimationType.fade,
|
|
||||||
// badgeColor: Colors.red,
|
|
||||||
// child: FloatingActionButton(
|
|
||||||
// child: const Icon(Icons.filter_alt_outlined),
|
|
||||||
// onPressed: _openDocumentFilter,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: NestedScrollView(
|
body: WillPopScope(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
onWillPop: () async {
|
||||||
return [
|
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||||
SliverAppBar(
|
context.read<DocumentsCubit>().resetSelection();
|
||||||
floating: true,
|
}
|
||||||
pinned: true,
|
return false;
|
||||||
snap: true,
|
},
|
||||||
title: SearchBar(
|
child: NestedScrollView(
|
||||||
height: kToolbarHeight - 2,
|
floatHeaderSlivers: true,
|
||||||
supportingText: "Search documents",
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
onTap: () {
|
SliverOverlapAbsorber(
|
||||||
showDocumentSearchPage(context);
|
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||||
},
|
// and redirects it to the SliverOverlapInjector below. If it is
|
||||||
leadingIcon: Icon(Icons.menu),
|
// missing, then it is possible for the nested "inner" scroll view
|
||||||
trailingIcon: CircleAvatar(
|
// below to end up under the SliverAppBar even when the inner
|
||||||
child: Text("A"),
|
// scroll view thinks it has not been scrolled.
|
||||||
|
// This is not necessary if the "headerSliverBuilder" only builds
|
||||||
|
// widgets that do not overlap the next sliver.
|
||||||
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
sliver: SearchAppBar(
|
||||||
|
onOpenSearch: showDocumentSearchPage,
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: S.of(context).documentsPageTitle),
|
||||||
|
Tab(text: S.of(context).savedViewsLabel),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
];
|
],
|
||||||
},
|
body: NotificationListener<ScrollUpdateNotification>(
|
||||||
body: WillPopScope(
|
onNotification: (notification) {
|
||||||
onWillPop: () async {
|
final metrics = notification.metrics;
|
||||||
if (context
|
final desiredTab =
|
||||||
.read<DocumentsCubit>()
|
(metrics.pixels / metrics.maxScrollExtent).round();
|
||||||
.state
|
if (metrics.axis == Axis.horizontal &&
|
||||||
.selection
|
_currentTab != desiredTab) {
|
||||||
.isNotEmpty) {
|
setState(() => _currentTab = desiredTab);
|
||||||
context.read<DocumentsCubit>().resetSelection();
|
}
|
||||||
}
|
return true;
|
||||||
return false;
|
},
|
||||||
},
|
child: NotificationListener<ScrollMetricsNotification>(
|
||||||
child: RefreshIndicator(
|
onNotification: (notification) {
|
||||||
onRefresh: _onRefresh,
|
// Listen for scroll notifications to load new data.
|
||||||
notificationPredicate: (_) => connectivityState.isConnected,
|
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
final currState = context.read<DocumentsCubit>().state;
|
||||||
builder: (context, taskState) {
|
final max = notification.metrics.maxScrollExtent;
|
||||||
return _buildBody(connectivityState);
|
if (max == 0 ||
|
||||||
// return Stack(
|
_currentTab != 0 ||
|
||||||
// children: [
|
currState.isLoading ||
|
||||||
// Positioned(
|
currState.isLastPageLoaded) {
|
||||||
// left: 0,
|
return true;
|
||||||
// right: 0,
|
}
|
||||||
// top: _offset,
|
final offset = notification.metrics.pixels;
|
||||||
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
if (offset >= max * 0.7) {
|
||||||
// builder: (context, state) {
|
context
|
||||||
// return ColoredBox(
|
.read<DocumentsCubit>()
|
||||||
// color:
|
.loadMore()
|
||||||
// Theme.of(context).colorScheme.background,
|
.onError<PaperlessServerException>(
|
||||||
// child: SavedViewSelectionWidget(
|
(error, stackTrace) => showErrorMessage(
|
||||||
// height: _savedViewWidgetHeight,
|
context,
|
||||||
// currentFilter: state.filter,
|
error,
|
||||||
// enabled: state.selection.isEmpty &&
|
stackTrace,
|
||||||
// connectivityState.isConnected,
|
),
|
||||||
// ),
|
);
|
||||||
// );
|
}
|
||||||
// },
|
return true;
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
},
|
},
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
edgeOffset: kToolbarHeight,
|
||||||
|
onRefresh: _onReloadDocuments,
|
||||||
|
notificationPredicate: (_) =>
|
||||||
|
connectivityState.isConnected,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>("documents"),
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView
|
||||||
|
.sliverOverlapAbsorberHandleFor(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
!const ListEquality().equals(
|
||||||
|
previous.documents,
|
||||||
|
current.documents,
|
||||||
|
) ||
|
||||||
|
previous.selectedIds !=
|
||||||
|
current.selectedIds,
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.hasLoaded &&
|
||||||
|
state.documents.isEmpty) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: DocumentsEmptyState(
|
||||||
|
state: state,
|
||||||
|
onReset: () {
|
||||||
|
context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.resetFilter();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return BlocBuilder<
|
||||||
|
ApplicationSettingsCubit,
|
||||||
|
ApplicationSettingsState>(
|
||||||
|
builder: (context, settings) {
|
||||||
|
return SliverAdaptiveDocumentsView(
|
||||||
|
viewType:
|
||||||
|
settings.preferredViewType,
|
||||||
|
onTap: _openDetails,
|
||||||
|
onSelected: context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.toggleDocumentSelection,
|
||||||
|
hasInternetConnection:
|
||||||
|
connectivityState.isConnected,
|
||||||
|
onTagSelected: _addTagToFilter,
|
||||||
|
onCorrespondentSelected:
|
||||||
|
_addCorrespondentToFilter,
|
||||||
|
onDocumentTypeSelected:
|
||||||
|
_addDocumentTypeToFilter,
|
||||||
|
onStoragePathSelected:
|
||||||
|
_addStoragePathToFilter,
|
||||||
|
documents: state.documents,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
isLabelClickable: true,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
selectedDocumentIds:
|
||||||
|
state.selectedIds,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
edgeOffset: kToolbarHeight,
|
||||||
|
onRefresh: _onReloadSavedViews,
|
||||||
|
notificationPredicate: (_) =>
|
||||||
|
connectivityState.isConnected,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>("savedViews"),
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView
|
||||||
|
.sliverOverlapAbsorberHandleFor(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
const SavedViewList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -293,28 +324,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>
|
//TODO: Add app bar...
|
||||||
_buildViewTypeButton() {
|
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
|
||||||
builder: (context, settingsState) => IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
settingsState.preferredViewType == ViewType.grid
|
|
||||||
? Icons.list
|
|
||||||
: Icons.grid_view_rounded,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
|
||||||
setState(() {
|
|
||||||
_offset = 0;
|
|
||||||
_last = 0;
|
|
||||||
});
|
|
||||||
final cubit = context.read<ApplicationSettingsCubit>();
|
|
||||||
cubit.setViewType(cubit.state.preferredViewType.toggle());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||||
final shouldDelete = await showDialog<bool>(
|
final shouldDelete = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -338,6 +348,25 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCreateSavedView(DocumentFilter filter) async {
|
||||||
|
final newView = await Navigator.of(context).push<SavedView?>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LabelsBlocProvider(
|
||||||
|
child: AddSavedViewPage(
|
||||||
|
currentFilter: filter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (newView != null) {
|
||||||
|
try {
|
||||||
|
await context.read<SavedViewCubit>().add(newView);
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _openDocumentFilter() async {
|
void _openDocumentFilter() async {
|
||||||
final draggableSheetController = DraggableScrollableController();
|
final draggableSheetController = DraggableScrollableController();
|
||||||
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
||||||
@@ -373,12 +402,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
try {
|
try {
|
||||||
if (filterIntent.shouldReset) {
|
if (filterIntent.shouldReset) {
|
||||||
await context.read<DocumentsCubit>().resetFilter();
|
await context.read<DocumentsCubit>().resetFilter();
|
||||||
context.read<DocumentsCubit>().unselectView();
|
|
||||||
} else {
|
} else {
|
||||||
if (filterIntent.filter !=
|
|
||||||
context.read<DocumentsCubit>().state.filter) {
|
|
||||||
context.read<DocumentsCubit>().unselectView();
|
|
||||||
}
|
|
||||||
await context
|
await context
|
||||||
.read<DocumentsCubit>()
|
.read<DocumentsCubit>()
|
||||||
.updateFilter(filter: filterIntent.filter!);
|
.updateFilter(filter: filterIntent.filter!);
|
||||||
@@ -389,75 +413,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDocumentCount(int count) {
|
|
||||||
return count > 99 ? "99+" : count.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBody(ConnectivityState connectivityState) {
|
|
||||||
final isConnected = connectivityState == ConnectivityState.connected;
|
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
|
||||||
builder: (context, settings) {
|
|
||||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
buildWhen: (previous, current) =>
|
|
||||||
!const ListEquality()
|
|
||||||
.equals(previous.documents, current.documents) ||
|
|
||||||
previous.selectedIds != current.selectedIds,
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
|
||||||
return DocumentsEmptyState(
|
|
||||||
state: state,
|
|
||||||
onReset: () {
|
|
||||||
context.read<DocumentsCubit>().resetFilter();
|
|
||||||
context.read<DocumentsCubit>().unselectView();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AdaptiveDocumentsView(
|
|
||||||
viewType: settings.preferredViewType,
|
|
||||||
state: state,
|
|
||||||
scrollController: _scrollController,
|
|
||||||
onTap: _openDetails,
|
|
||||||
onSelected: _onSelected,
|
|
||||||
hasInternetConnection: isConnected,
|
|
||||||
onTagSelected: _addTagToFilter,
|
|
||||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
|
||||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
|
||||||
onStoragePathSelected: _addStoragePathToFilter,
|
|
||||||
pageLoadingWidget: const NewItemsLoadingWidget(),
|
|
||||||
beforeItems: SizedBox(
|
|
||||||
height: kToolbarHeight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const SortDocumentsButton(),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
settings.preferredViewType == ViewType.grid
|
|
||||||
? Icons.list
|
|
||||||
: Icons.grid_view_rounded,
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
context.read<ApplicationSettingsCubit>().setViewType(
|
|
||||||
settings.preferredViewType.toggle(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openDetails(DocumentModel document) async {
|
Future<void> _openDetails(DocumentModel document) async {
|
||||||
final potentiallyUpdatedModel =
|
final updatedModel = await Navigator.of(context).push<DocumentModel?>(
|
||||||
await Navigator.of(context).push<DocumentModel?>(
|
|
||||||
_buildDetailsPageRoute(document),
|
_buildDetailsPageRoute(document),
|
||||||
);
|
);
|
||||||
if (potentiallyUpdatedModel != document) {
|
if (updatedModel != document) {
|
||||||
context.read<DocumentsCubit>().reload();
|
context.read<DocumentsCubit>().reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,15 +518,19 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelected(DocumentModel model) {
|
Future<void> _onReloadDocuments() async {
|
||||||
context.read<DocumentsCubit>().toggleDocumentSelection(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onRefresh() async {
|
|
||||||
try {
|
try {
|
||||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||||
context.read<DocumentsCubit>().reload();
|
await context.read<DocumentsCubit>().reload();
|
||||||
context.read<SavedViewCubit>().reload();
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onReloadSavedViews() async {
|
||||||
|
try {
|
||||||
|
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||||
|
await context.read<SavedViewCubit>().reload();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
|
|||||||
232
lib/features/documents/view/widgets/adaptive_documents_view.dart
Normal file
232
lib/features/documents/view/widgets/adaptive_documents_view.dart
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||||
|
final List<DocumentModel> documents;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool hasLoaded;
|
||||||
|
final bool enableHeroAnimation;
|
||||||
|
final List<int> selectedDocumentIds;
|
||||||
|
final ViewType viewType;
|
||||||
|
final void Function(DocumentModel)? onTap;
|
||||||
|
final void Function(DocumentModel)? onSelected;
|
||||||
|
final bool hasInternetConnection;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
final void Function(int id)? onTagSelected;
|
||||||
|
final void Function(int? id)? onCorrespondentSelected;
|
||||||
|
final void Function(int? id)? onDocumentTypeSelected;
|
||||||
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
|
||||||
|
const AdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
this.selectedDocumentIds = const [],
|
||||||
|
required this.documents,
|
||||||
|
this.onTap,
|
||||||
|
this.onSelected,
|
||||||
|
this.viewType = ViewType.list,
|
||||||
|
required this.hasInternetConnection,
|
||||||
|
required this.isLabelClickable,
|
||||||
|
this.onTagSelected,
|
||||||
|
this.onCorrespondentSelected,
|
||||||
|
this.onDocumentTypeSelected,
|
||||||
|
this.onStoragePathSelected,
|
||||||
|
required this.isLoading,
|
||||||
|
required this.hasLoaded,
|
||||||
|
this.enableHeroAnimation = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||||
|
const SliverAdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
required super.documents,
|
||||||
|
required super.hasInternetConnection,
|
||||||
|
required super.isLabelClickable,
|
||||||
|
super.onCorrespondentSelected,
|
||||||
|
super.onDocumentTypeSelected,
|
||||||
|
super.onStoragePathSelected,
|
||||||
|
super.onSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
super.selectedDocumentIds,
|
||||||
|
super.viewType,
|
||||||
|
required super.isLoading,
|
||||||
|
required super.hasLoaded,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (viewType) {
|
||||||
|
case ViewType.grid:
|
||||||
|
return _buildGridView();
|
||||||
|
case ViewType.list:
|
||||||
|
return _buildListView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListView() {
|
||||||
|
if (!hasLoaded && isLoading) {
|
||||||
|
return const DocumentsListLoadingWidget();
|
||||||
|
}
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
childCount: documents.length,
|
||||||
|
(context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return LabelRepositoriesProvider(
|
||||||
|
child: DocumentListItem(
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridView() {
|
||||||
|
if (!hasLoaded && isLoading) {
|
||||||
|
return const DocumentsListLoadingWidget();
|
||||||
|
}
|
||||||
|
return SliverGrid.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 1 / 2,
|
||||||
|
),
|
||||||
|
itemCount: documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return DocumentGridItem(
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
const DefaultAdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
required super.documents,
|
||||||
|
required super.hasInternetConnection,
|
||||||
|
required super.isLabelClickable,
|
||||||
|
required super.isLoading,
|
||||||
|
required super.hasLoaded,
|
||||||
|
super.onCorrespondentSelected,
|
||||||
|
super.onDocumentTypeSelected,
|
||||||
|
super.onStoragePathSelected,
|
||||||
|
super.onSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
this.scrollController,
|
||||||
|
super.selectedDocumentIds,
|
||||||
|
super.viewType,
|
||||||
|
super.enableHeroAnimation = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (viewType) {
|
||||||
|
case ViewType.grid:
|
||||||
|
return _buildGridView();
|
||||||
|
case ViewType.list:
|
||||||
|
return _buildListView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListView() {
|
||||||
|
if (!hasLoaded && isLoading) {
|
||||||
|
return const CustomScrollView(slivers: [
|
||||||
|
DocumentsListLoadingWidget(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
primary: false,
|
||||||
|
itemCount: documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return LabelRepositoriesProvider(
|
||||||
|
child: DocumentListItem(
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridView() {
|
||||||
|
if (!hasLoaded && isLoading) {
|
||||||
|
return const CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
DocumentsListLoadingWidget(),
|
||||||
|
],
|
||||||
|
); //TODO: Build grid skeleton
|
||||||
|
}
|
||||||
|
return GridView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
primary: false,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 1 / 2,
|
||||||
|
),
|
||||||
|
itemCount: documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return DocumentGridItem(
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,11 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
|
|
||||||
class DocumentsEmptyState extends StatelessWidget {
|
class DocumentsEmptyState extends StatelessWidget {
|
||||||
final PagedDocumentsState state;
|
final PagedDocumentsState state;
|
||||||
final VoidCallback onReset;
|
final VoidCallback? onReset;
|
||||||
const DocumentsEmptyState({
|
const DocumentsEmptyState({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.onReset,
|
this.onReset,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -20,7 +20,7 @@ class DocumentsEmptyState extends StatelessWidget {
|
|||||||
child: EmptyState(
|
child: EmptyState(
|
||||||
title: S.of(context).documentsPageEmptyStateOopsText,
|
title: S.of(context).documentsPageEmptyStateOopsText,
|
||||||
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
|
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
|
||||||
bottomChild: state.filter != DocumentFilter.initial
|
bottomChild: state.filter != DocumentFilter.initial && onReset != null
|
||||||
? TextButton(
|
? TextButton(
|
||||||
onPressed: onReset,
|
onPressed: onReset,
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -5,37 +5,23 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||||
final List<Widget> beforeWidgets;
|
|
||||||
final List<Widget> afterWidgets;
|
|
||||||
|
|
||||||
static const _tags = [" ", " ", " "];
|
static const _tags = [" ", " ", " "];
|
||||||
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
||||||
static const _correspondentLengths = <double>[200.0, 300.0, 150.0];
|
static const _correspondentLengths = <double>[200.0, 300.0, 150.0];
|
||||||
static const _fontSize = 16.0;
|
static const _fontSize = 16.0;
|
||||||
|
|
||||||
const DocumentsListLoadingWidget({
|
const DocumentsListLoadingWidget({super.key
|
||||||
super.key,
|
|
||||||
this.beforeWidgets = const [],
|
|
||||||
this.afterWidgets = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final _random = Random();
|
final _random = Random();
|
||||||
return CustomScrollView(
|
return SliverList(
|
||||||
slivers: [
|
delegate: SliverChildBuilderDelegate(
|
||||||
SliverList(
|
(context, index) {
|
||||||
delegate: SliverChildListDelegate(beforeWidgets),
|
return _buildFakeListItem(context, _random);
|
||||||
),
|
},
|
||||||
SliverList(
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
return _buildFakeListItem(context, _random);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverList(delegate: SliverChildListDelegate(afterWidgets))
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,38 +1,35 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DocumentGridItem extends StatelessWidget {
|
class DocumentGridItem extends DocumentItem {
|
||||||
final DocumentModel document;
|
|
||||||
final bool isSelected;
|
|
||||||
final void Function(DocumentModel) onTap;
|
|
||||||
final void Function(DocumentModel) onSelected;
|
|
||||||
final bool isAtLeastOneSelected;
|
|
||||||
final bool Function(int tagId) isTagSelectedPredicate;
|
|
||||||
final void Function(int tagId)? onTagSelected;
|
|
||||||
|
|
||||||
const DocumentGridItem({
|
const DocumentGridItem({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.document,
|
required super.document,
|
||||||
required this.onTap,
|
required super.isSelected,
|
||||||
required this.onSelected,
|
required super.isSelectionActive,
|
||||||
required this.isSelected,
|
required super.isLabelClickable,
|
||||||
required this.isAtLeastOneSelected,
|
super.onCorrespondentSelected,
|
||||||
required this.isTagSelectedPredicate,
|
super.onDocumentTypeSelected,
|
||||||
required this.onTagSelected,
|
super.onSelected,
|
||||||
}) : super(key: key);
|
super.onStoragePathSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
required super.enableHeroAnimation,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onTap,
|
onTap: _onTap,
|
||||||
onLongPress: () => onSelected(document),
|
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: isAtLeastOneSelected,
|
absorbing: isSelectionActive,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -48,6 +45,7 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
child: DocumentPreview(
|
child: DocumentPreview(
|
||||||
id: document.id,
|
id: document.id,
|
||||||
borderRadius: 12.0,
|
borderRadius: 12.0,
|
||||||
|
enableHero: enableHeroAnimation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -94,10 +92,10 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTap() {
|
void _onTap() {
|
||||||
if (isAtLeastOneSelected || isSelected) {
|
if (isSelectionActive || isSelected) {
|
||||||
onSelected(document);
|
onSelected?.call(document);
|
||||||
} else {
|
} else {
|
||||||
onTap(document);
|
onTap?.call(document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
lib/features/documents/view/widgets/items/document_item.dart
Normal file
32
lib/features/documents/view/widgets/items/document_item.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
|
abstract class DocumentItem extends StatelessWidget {
|
||||||
|
final DocumentModel document;
|
||||||
|
final void Function(DocumentModel)? onTap;
|
||||||
|
final void Function(DocumentModel)? onSelected;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool isSelectionActive;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
final bool enableHeroAnimation;
|
||||||
|
|
||||||
|
final void Function(int tagId)? onTagSelected;
|
||||||
|
final void Function(int? correspondentId)? onCorrespondentSelected;
|
||||||
|
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
||||||
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
|
||||||
|
const DocumentItem({
|
||||||
|
super.key,
|
||||||
|
required this.document,
|
||||||
|
this.onTap,
|
||||||
|
this.onSelected,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.isSelectionActive,
|
||||||
|
required this.isLabelClickable,
|
||||||
|
this.onTagSelected,
|
||||||
|
this.onCorrespondentSelected,
|
||||||
|
this.onDocumentTypeSelected,
|
||||||
|
this.onStoragePathSelected,
|
||||||
|
required this.enableHeroAnimation,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,39 +1,26 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
class DocumentListItem extends StatelessWidget {
|
class DocumentListItem extends DocumentItem {
|
||||||
static const _a4AspectRatio = 1 / 1.4142;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
final DocumentModel document;
|
|
||||||
final void Function(DocumentModel)? onTap;
|
|
||||||
final void Function(DocumentModel)? onSelected;
|
|
||||||
final bool isSelected;
|
|
||||||
final bool isAtLeastOneSelected;
|
|
||||||
final bool isLabelClickable;
|
|
||||||
|
|
||||||
final void Function(int tagId)? onTagSelected;
|
|
||||||
final void Function(int? correspondentId)? onCorrespondentSelected;
|
|
||||||
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
|
||||||
|
|
||||||
final bool enableHeroAnimation;
|
|
||||||
|
|
||||||
const DocumentListItem({
|
const DocumentListItem({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.document,
|
required super.document,
|
||||||
this.onTap,
|
required super.isSelected,
|
||||||
this.onSelected,
|
required super.isSelectionActive,
|
||||||
this.isSelected = false,
|
required super.isLabelClickable,
|
||||||
this.isAtLeastOneSelected = false,
|
super.onCorrespondentSelected,
|
||||||
this.isLabelClickable = true,
|
super.onDocumentTypeSelected,
|
||||||
this.onTagSelected,
|
super.onSelected,
|
||||||
this.onCorrespondentSelected,
|
super.onStoragePathSelected,
|
||||||
this.onDocumentTypeSelected,
|
super.onTagSelected,
|
||||||
this.onStoragePathSelected,
|
super.onTap,
|
||||||
this.enableHeroAnimation = true,
|
super.enableHeroAnimation = true,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -50,7 +37,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: isAtLeastOneSelected,
|
absorbing: isSelectionActive,
|
||||||
child: CorrespondentWidget(
|
child: CorrespondentWidget(
|
||||||
isClickable: isLabelClickable,
|
isClickable: isLabelClickable,
|
||||||
correspondentId: document.correspondent,
|
correspondentId: document.correspondent,
|
||||||
@@ -69,7 +56,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: isAtLeastOneSelected,
|
absorbing: isSelectionActive,
|
||||||
child: TagsWidget(
|
child: TagsWidget(
|
||||||
isClickable: isLabelClickable,
|
isClickable: isLabelClickable,
|
||||||
tagIds: document.tags,
|
tagIds: document.tags,
|
||||||
@@ -95,7 +82,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTap() {
|
void _onTap() {
|
||||||
if (isAtLeastOneSelected || isSelected) {
|
if (isSelectionActive || isSelected) {
|
||||||
onSelected?.call(document);
|
onSelected?.call(document);
|
||||||
} else {
|
} else {
|
||||||
onTap?.call(document);
|
onTap?.call(document);
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|
||||||
|
|
||||||
class AdaptiveDocumentsView extends StatelessWidget {
|
|
||||||
final DocumentsState state;
|
|
||||||
final ViewType viewType;
|
|
||||||
final Widget? beforeItems;
|
|
||||||
final void Function(DocumentModel) onTap;
|
|
||||||
final void Function(DocumentModel) onSelected;
|
|
||||||
final ScrollController scrollController;
|
|
||||||
final bool hasInternetConnection;
|
|
||||||
final bool isLabelClickable;
|
|
||||||
final void Function(int id)? onTagSelected;
|
|
||||||
final void Function(int? id)? onCorrespondentSelected;
|
|
||||||
final void Function(int? id)? onDocumentTypeSelected;
|
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
|
||||||
final Widget pageLoadingWidget;
|
|
||||||
|
|
||||||
const AdaptiveDocumentsView({
|
|
||||||
super.key,
|
|
||||||
required this.onTap,
|
|
||||||
required this.scrollController,
|
|
||||||
required this.state,
|
|
||||||
required this.onSelected,
|
|
||||||
required this.hasInternetConnection,
|
|
||||||
this.isLabelClickable = true,
|
|
||||||
this.onTagSelected,
|
|
||||||
this.onCorrespondentSelected,
|
|
||||||
this.onDocumentTypeSelected,
|
|
||||||
this.onStoragePathSelected,
|
|
||||||
required this.pageLoadingWidget,
|
|
||||||
this.beforeItems,
|
|
||||||
required this.viewType,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(child: beforeItems),
|
|
||||||
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
|
|
||||||
if (state.hasLoaded && state.isLoading)
|
|
||||||
SliverToBoxAdapter(child: pageLoadingWidget),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SliverList _buildListView() {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
childCount: state.documents.length,
|
|
||||||
(context, index) {
|
|
||||||
final document = state.documents.elementAt(index);
|
|
||||||
return LabelRepositoriesProvider(
|
|
||||||
child: DocumentListItem(
|
|
||||||
isLabelClickable: isLabelClickable,
|
|
||||||
document: document,
|
|
||||||
onTap: onTap,
|
|
||||||
isSelected: state.selectedIds.contains(document.id),
|
|
||||||
onSelected: onSelected,
|
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
|
||||||
onTagSelected: onTagSelected,
|
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildGridView() {
|
|
||||||
return SliverGrid.builder(
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 4,
|
|
||||||
crossAxisSpacing: 4,
|
|
||||||
childAspectRatio: 1 / 2,
|
|
||||||
),
|
|
||||||
itemCount: state.documents.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (state.hasLoaded &&
|
|
||||||
state.isLoading &&
|
|
||||||
index == state.documents.length) {
|
|
||||||
return Center(child: pageLoadingWidget);
|
|
||||||
}
|
|
||||||
final document = state.documents.elementAt(index);
|
|
||||||
return DocumentGridItem(
|
|
||||||
document: document,
|
|
||||||
onTap: onTap,
|
|
||||||
isSelected: state.selectedIds.contains(document.id),
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
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/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';
|
||||||
|
|
||||||
|
import 'text_query_form_field.dart';
|
||||||
|
|
||||||
|
class DocumentFilterForm extends StatefulWidget {
|
||||||
|
static const fkCorrespondent = DocumentModel.correspondentKey;
|
||||||
|
static const fkDocumentType = DocumentModel.documentTypeKey;
|
||||||
|
static const fkStoragePath = DocumentModel.storagePathKey;
|
||||||
|
static const fkQuery = "query";
|
||||||
|
static const fkCreatedAt = DocumentModel.createdKey;
|
||||||
|
static const fkAddedAt = DocumentModel.addedKey;
|
||||||
|
|
||||||
|
static DocumentFilter assembleFilter(
|
||||||
|
GlobalKey<FormBuilderState> formKey, DocumentFilter initialFilter) {
|
||||||
|
formKey.currentState?.save();
|
||||||
|
final v = formKey.currentState!.value;
|
||||||
|
return DocumentFilter(
|
||||||
|
correspondent:
|
||||||
|
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
|
||||||
|
DocumentFilter.initial.correspondent,
|
||||||
|
documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ??
|
||||||
|
DocumentFilter.initial.documentType,
|
||||||
|
storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ??
|
||||||
|
DocumentFilter.initial.storagePath,
|
||||||
|
tags:
|
||||||
|
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
||||||
|
query: v[DocumentFilterForm.fkQuery] as TextQuery? ??
|
||||||
|
DocumentFilter.initial.query,
|
||||||
|
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
|
||||||
|
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
|
||||||
|
asnQuery: initialFilter.asnQuery,
|
||||||
|
page: 1,
|
||||||
|
pageSize: initialFilter.pageSize,
|
||||||
|
sortField: initialFilter.sortField,
|
||||||
|
sortOrder: initialFilter.sortOrder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Widget? header;
|
||||||
|
final GlobalKey<FormBuilderState> formKey;
|
||||||
|
final DocumentFilter initialFilter;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
const DocumentFilterForm({
|
||||||
|
super.key,
|
||||||
|
this.header,
|
||||||
|
required this.formKey,
|
||||||
|
required this.initialFilter,
|
||||||
|
this.scrollController,
|
||||||
|
this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DocumentFilterForm> createState() => _DocumentFilterFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||||
|
late bool _allowOnlyExtendedQuery;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FormBuilder(
|
||||||
|
key: widget.formKey,
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
slivers: [
|
||||||
|
if (widget.header != null) widget.header!,
|
||||||
|
..._buildFormFieldList(),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildFormFieldList() {
|
||||||
|
return [
|
||||||
|
_buildQueryFormField(),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentFilterAdvancedLabel,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FormBuilderExtendedDateRangePicker(
|
||||||
|
name: DocumentFilterForm.fkCreatedAt,
|
||||||
|
initialValue: widget.initialFilter.created,
|
||||||
|
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||||
|
onChanged: (_) {
|
||||||
|
_checkQueryConstraints();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FormBuilderExtendedDateRangePicker(
|
||||||
|
name: DocumentFilterForm.fkAddedAt,
|
||||||
|
initialValue: widget.initialFilter.added,
|
||||||
|
labelText: S.of(context).documentAddedPropertyLabel,
|
||||||
|
onChanged: (_) {
|
||||||
|
_checkQueryConstraints();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildCorrespondentFormField(),
|
||||||
|
_buildDocumentTypeFormField(),
|
||||||
|
_buildStoragePathFormField(),
|
||||||
|
_buildTagsFormField(),
|
||||||
|
]
|
||||||
|
.map((w) => SliverPadding(
|
||||||
|
padding: widget.padding,
|
||||||
|
sliver: SliverToBoxAdapter(child: w),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkQueryConstraints() {
|
||||||
|
final filter =
|
||||||
|
DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
|
||||||
|
if (filter.forceExtendedQuery) {
|
||||||
|
setState(() => _allowOnlyExtendedQuery = true);
|
||||||
|
final queryField =
|
||||||
|
widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
|
||||||
|
queryField?.didChange(
|
||||||
|
(queryField.value as TextQuery?)
|
||||||
|
?.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setState(() => _allowOnlyExtendedQuery = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDocumentTypeFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return LabelFormField<DocumentType>(
|
||||||
|
formBuilderState: widget.formKey.currentState,
|
||||||
|
name: DocumentFilterForm.fkDocumentType,
|
||||||
|
labelOptions: state.labels,
|
||||||
|
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
||||||
|
initialValue: widget.initialFilter.documentType,
|
||||||
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCorrespondentFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return LabelFormField<Correspondent>(
|
||||||
|
formBuilderState: widget.formKey.currentState,
|
||||||
|
name: DocumentFilterForm.fkCorrespondent,
|
||||||
|
labelOptions: state.labels,
|
||||||
|
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
|
initialValue: widget.initialFilter.correspondent,
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStoragePathFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return LabelFormField<StoragePath>(
|
||||||
|
formBuilderState: widget.formKey.currentState,
|
||||||
|
name: DocumentFilterForm.fkStoragePath,
|
||||||
|
labelOptions: state.labels,
|
||||||
|
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
||||||
|
initialValue: widget.initialFilter.storagePath,
|
||||||
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQueryFormField() {
|
||||||
|
return TextQueryFormField(
|
||||||
|
name: DocumentFilterForm.fkQuery,
|
||||||
|
onlyExtendedQueryAllowed: _allowOnlyExtendedQuery,
|
||||||
|
initialValue: widget.initialFilter.query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return TagFormField(
|
||||||
|
name: DocumentModel.tagsKey,
|
||||||
|
initialValue: widget.initialFilter.tags,
|
||||||
|
allowCreation: false,
|
||||||
|
selectableOptions: state.labels,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ 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/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/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.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_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||||
@@ -32,22 +33,14 @@ class DocumentFilterPanel extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||||
static const fkCorrespondent = DocumentModel.correspondentKey;
|
|
||||||
static const fkDocumentType = DocumentModel.documentTypeKey;
|
|
||||||
static const fkStoragePath = DocumentModel.storagePathKey;
|
|
||||||
static const fkQuery = "query";
|
|
||||||
static const fkCreatedAt = DocumentModel.createdKey;
|
|
||||||
static const fkAddedAt = DocumentModel.addedKey;
|
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
late bool _allowOnlyExtendedQuery;
|
|
||||||
|
|
||||||
double _heightAnimationValue = 0;
|
double _heightAnimationValue = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
|
|
||||||
widget.draggableSheetController.addListener(animateTitleByDrag);
|
widget.draggableSheetController.addListener(animateTitleByDrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,100 +99,59 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: FormBuilder(
|
body: DocumentFilterForm(
|
||||||
key: _formKey,
|
formKey: _formKey,
|
||||||
child: _buildFormList(context),
|
scrollController: widget.scrollController,
|
||||||
|
initialFilter: widget.initialFilter,
|
||||||
|
header: _buildPanelHeader(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFormList(BuildContext context) {
|
Widget _buildPanelHeader() {
|
||||||
return CustomScrollView(
|
return SliverAppBar(
|
||||||
controller: widget.scrollController,
|
pinned: true,
|
||||||
slivers: [
|
automaticallyImplyLeading: false,
|
||||||
SliverAppBar(
|
toolbarHeight: kToolbarHeight + 22,
|
||||||
pinned: true,
|
title: SizedBox(
|
||||||
automaticallyImplyLeading: false,
|
width: MediaQuery.of(context).size.width,
|
||||||
toolbarHeight: kToolbarHeight + 22,
|
child: Column(
|
||||||
title: SizedBox(
|
mainAxisSize: MainAxisSize.max,
|
||||||
width: MediaQuery.of(context).size.width,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Column(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.max,
|
Opacity(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
opacity: 1 - _heightAnimationValue,
|
||||||
children: [
|
child: Padding(
|
||||||
Opacity(
|
padding: const EdgeInsets.only(bottom: 11),
|
||||||
opacity: 1 - _heightAnimationValue,
|
child: _buildDragHandle(),
|
||||||
child: Padding(
|
),
|
||||||
padding: EdgeInsets.only(bottom: 11),
|
|
||||||
child: _buildDragHandle(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
children: [
|
|
||||||
Opacity(
|
|
||||||
opacity: max(0, (_heightAnimationValue - 0.5) * 2),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Navigator.of(context).pop(),
|
|
||||||
child: const Icon(Icons.expand_more_rounded),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(left: _heightAnimationValue * 48),
|
|
||||||
child: Text(S.of(context).documentFilterTitle),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: max(0, (_heightAnimationValue - 0.5) * 2),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Navigator.of(context).pop(),
|
||||||
|
child: const Icon(Icons.expand_more_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: _heightAnimationValue * 48),
|
||||||
|
child: Text(S.of(context).documentFilterTitle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
..._buildFormFieldList(),
|
),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildFormFieldList() {
|
|
||||||
return [
|
|
||||||
_buildQueryFormField().paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
S.of(context).documentFilterAdvancedLabel,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
).paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
FormBuilderExtendedDateRangePicker(
|
|
||||||
name: fkCreatedAt,
|
|
||||||
initialValue: widget.initialFilter.created,
|
|
||||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
|
||||||
onChanged: (_) {
|
|
||||||
_checkQueryConstraints();
|
|
||||||
},
|
|
||||||
).paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
FormBuilderExtendedDateRangePicker(
|
|
||||||
name: fkAddedAt,
|
|
||||||
initialValue: widget.initialFilter.added,
|
|
||||||
labelText: S.of(context).documentAddedPropertyLabel,
|
|
||||||
onChanged: (_) {
|
|
||||||
_checkQueryConstraints();
|
|
||||||
},
|
|
||||||
).paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildCorrespondentFormField()
|
|
||||||
.paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildDocumentTypeFormField()
|
|
||||||
.paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildStoragePathFormField()
|
|
||||||
.paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildTagsFormField().padded(16),
|
|
||||||
].map((w) => SliverToBoxAdapter(child: w)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Container _buildDragHandle() {
|
Container _buildDragHandle() {
|
||||||
return Container(
|
return Container(
|
||||||
// According to m3 spec https://m3.material.io/components/bottom-sheets/specs
|
// According to m3 spec https://m3.material.io/components/bottom-sheets/specs
|
||||||
@@ -212,19 +164,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return TagFormField(
|
|
||||||
name: DocumentModel.tagsKey,
|
|
||||||
initialValue: widget.initialFilter.tags,
|
|
||||||
allowCreation: false,
|
|
||||||
selectableOptions: state.labels,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _resetFilter() async {
|
void _resetFilter() async {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
@@ -233,102 +172,13 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentTypeFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return LabelFormField<DocumentType>(
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
name: fkDocumentType,
|
|
||||||
labelOptions: state.labels,
|
|
||||||
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
|
||||||
initialValue: widget.initialFilter.documentType,
|
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCorrespondentFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return LabelFormField<Correspondent>(
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
name: fkCorrespondent,
|
|
||||||
labelOptions: state.labels,
|
|
||||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
|
||||||
initialValue: widget.initialFilter.correspondent,
|
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStoragePathFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return LabelFormField<StoragePath>(
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
name: fkStoragePath,
|
|
||||||
labelOptions: state.labels,
|
|
||||||
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
|
||||||
initialValue: widget.initialFilter.storagePath,
|
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQueryFormField() {
|
|
||||||
return TextQueryFormField(
|
|
||||||
name: fkQuery,
|
|
||||||
onlyExtendedQueryAllowed: _allowOnlyExtendedQuery,
|
|
||||||
initialValue: widget.initialFilter.query,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onApplyFilter() async {
|
void _onApplyFilter() async {
|
||||||
_formKey.currentState?.save();
|
_formKey.currentState?.save();
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
DocumentFilter newFilter = _assembleFilter();
|
DocumentFilter newFilter =
|
||||||
|
DocumentFilterForm.assembleFilter(_formKey, widget.initialFilter);
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
Navigator.pop(context, DocumentFilterIntent(filter: newFilter));
|
Navigator.pop(context, DocumentFilterIntent(filter: newFilter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentFilter _assembleFilter() {
|
|
||||||
_formKey.currentState?.save();
|
|
||||||
final v = _formKey.currentState!.value;
|
|
||||||
return DocumentFilter(
|
|
||||||
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
|
|
||||||
DocumentFilter.initial.correspondent,
|
|
||||||
documentType: v[fkDocumentType] as IdQueryParameter? ??
|
|
||||||
DocumentFilter.initial.documentType,
|
|
||||||
storagePath: v[fkStoragePath] as IdQueryParameter? ??
|
|
||||||
DocumentFilter.initial.storagePath,
|
|
||||||
tags:
|
|
||||||
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
|
||||||
query: v[fkQuery] as TextQuery? ?? DocumentFilter.initial.query,
|
|
||||||
created: (v[fkCreatedAt] as DateRangeQuery),
|
|
||||||
added: (v[fkAddedAt] as DateRangeQuery),
|
|
||||||
asnQuery: widget.initialFilter.asnQuery,
|
|
||||||
page: 1,
|
|
||||||
pageSize: widget.initialFilter.pageSize,
|
|
||||||
sortField: widget.initialFilter.sortField,
|
|
||||||
sortOrder: widget.initialFilter.sortOrder,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _checkQueryConstraints() {
|
|
||||||
final filter = _assembleFilter();
|
|
||||||
if (filter.forceExtendedQuery) {
|
|
||||||
setState(() => _allowOnlyExtendedQuery = true);
|
|
||||||
final queryField = _formKey.currentState?.fields[fkQuery];
|
|
||||||
queryField?.didChange(
|
|
||||||
(queryField.value as TextQuery?)
|
|
||||||
?.copyWith(queryType: QueryType.extended),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setState(() => _allowOnlyExtendedQuery = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
39
lib/features/documents/view/widgets/view_actions.dart
Normal file
39
lib/features/documents/view/widgets/view_actions.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
class ViewActions extends StatelessWidget {
|
||||||
|
const ViewActions({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SortDocumentsButton(),
|
||||||
|
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
|
builder: (context, settings) {
|
||||||
|
final cubit = context.read<ApplicationSettingsCubit>();
|
||||||
|
switch (settings.preferredViewType) {
|
||||||
|
case ViewType.grid:
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.list),
|
||||||
|
onPressed: () =>
|
||||||
|
cubit.setViewType(settings.preferredViewType.toggle()),
|
||||||
|
);
|
||||||
|
case ViewType.list:
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.grid_view_rounded),
|
||||||
|
onPressed: () =>
|
||||||
|
cubit.setViewType(settings.preferredViewType.toggle()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.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/view/widgets/documents_list_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.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';
|
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
|
||||||
import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart';
|
import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart';
|
import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
@@ -48,14 +50,17 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
),
|
),
|
||||||
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (!state.hasLoaded) {
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
return const DocumentsListLoadingWidget();
|
builder: (context, connectivity) {
|
||||||
}
|
return DefaultAdaptiveDocumentsView(
|
||||||
return ListView.builder(
|
scrollController: _scrollController,
|
||||||
itemCount: state.documents.length,
|
documents: state.documents,
|
||||||
itemBuilder: (context, index) => DocumentListItem(
|
hasInternetConnection: connectivity.isConnected,
|
||||||
document: state.documents[index],
|
isLabelClickable: false,
|
||||||
),
|
isLoading: state.isLoading,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
20
lib/features/saved_view/cubit/saved_view_details_cubit.dart
Normal file
20
lib/features/saved_view/cubit/saved_view_details_cubit.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||||
|
|
||||||
|
part 'saved_view_details_state.dart';
|
||||||
|
|
||||||
|
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
|
||||||
|
with PagedDocumentsMixin {
|
||||||
|
@override
|
||||||
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
|
final SavedView savedView;
|
||||||
|
SavedViewDetailsCubit(
|
||||||
|
this.api, {
|
||||||
|
required this.savedView,
|
||||||
|
}) : super(const SavedViewDetailsState()) {
|
||||||
|
updateFilter(filter: savedView.toDocumentFilter());
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lib/features/saved_view/cubit/saved_view_details_state.dart
Normal file
47
lib/features/saved_view/cubit/saved_view_details_state.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
part of 'saved_view_details_cubit.dart';
|
||||||
|
|
||||||
|
class SavedViewDetailsState extends PagedDocumentsState {
|
||||||
|
const SavedViewDetailsState({
|
||||||
|
super.filter,
|
||||||
|
super.hasLoaded,
|
||||||
|
super.isLoading,
|
||||||
|
super.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
filter,
|
||||||
|
hasLoaded,
|
||||||
|
isLoading,
|
||||||
|
value,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
SavedViewDetailsState copyWithPaged({
|
||||||
|
bool? hasLoaded,
|
||||||
|
bool? isLoading,
|
||||||
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
|
DocumentFilter? filter,
|
||||||
|
}) {
|
||||||
|
return copyWith(
|
||||||
|
hasLoaded: hasLoaded,
|
||||||
|
isLoading: isLoading,
|
||||||
|
value: value,
|
||||||
|
filter: filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SavedViewDetailsState copyWith({
|
||||||
|
bool? hasLoaded,
|
||||||
|
bool? isLoading,
|
||||||
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
|
DocumentFilter? filter,
|
||||||
|
}) {
|
||||||
|
return SavedViewDetailsState(
|
||||||
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
value: value ?? this.value,
|
||||||
|
filter: filter ?? this.filter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
|
|
||||||
@@ -17,21 +20,13 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
|||||||
static const fkShowOnDashboard = 'show_on_dashboard';
|
static const fkShowOnDashboard = 'show_on_dashboard';
|
||||||
static const fkShowInSidebar = 'show_in_sidebar';
|
static const fkShowInSidebar = 'show_in_sidebar';
|
||||||
|
|
||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final _savedViewFormKey = GlobalKey<FormBuilderState>();
|
||||||
|
final _filterFormKey = GlobalKey<FormBuilderState>();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(S.of(context).savedViewCreateNewLabel),
|
title: Text(S.of(context).savedViewCreateNewLabel),
|
||||||
actions: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Tooltip(
|
|
||||||
child: const Icon(Icons.info_outline),
|
|
||||||
message: S.of(context).savedViewCreateTooltipText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
@@ -40,44 +35,102 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
|||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: FormBuilder(
|
child: Column(
|
||||||
key: _formKey,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: ListView(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
FormBuilderTextField(
|
FormBuilder(
|
||||||
name: fkName,
|
key: _savedViewFormKey,
|
||||||
validator: FormBuilderValidators.required(),
|
child: Column(
|
||||||
decoration: InputDecoration(
|
children: [
|
||||||
label: Text(S.of(context).savedViewNameLabel),
|
FormBuilderTextField(
|
||||||
),
|
name: _AddSavedViewPageState.fkName,
|
||||||
|
validator: FormBuilderValidators.required(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(S.of(context).savedViewNameLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FormBuilderCheckbox(
|
||||||
|
name: _AddSavedViewPageState.fkShowOnDashboard,
|
||||||
|
initialValue: false,
|
||||||
|
title: Text(S.of(context).savedViewShowOnDashboardLabel),
|
||||||
|
),
|
||||||
|
FormBuilderCheckbox(
|
||||||
|
name: _AddSavedViewPageState.fkShowInSidebar,
|
||||||
|
initialValue: false,
|
||||||
|
title: Text(S.of(context).savedViewShowInSidebarLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
),
|
||||||
name: fkShowOnDashboard,
|
Divider(),
|
||||||
initialValue: false,
|
Text(
|
||||||
title: Text(S.of(context).savedViewShowOnDashboardLabel),
|
"Review filter",
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
).padded(),
|
||||||
|
Flexible(
|
||||||
|
child: DocumentFilterForm(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
formKey: _filterFormKey,
|
||||||
|
initialFilter: widget.currentFilter,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
),
|
||||||
name: fkShowInSidebar,
|
],
|
||||||
initialValue: false,
|
|
||||||
title: Text(S.of(context).savedViewShowInSidebarLabel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Padding _buildOld(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FormBuilder(
|
||||||
|
key: _savedViewFormKey,
|
||||||
|
child: Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: fkName,
|
||||||
|
validator: FormBuilderValidators.required(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(S.of(context).savedViewNameLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FormBuilderCheckbox(
|
||||||
|
name: fkShowOnDashboard,
|
||||||
|
initialValue: false,
|
||||||
|
title: Text(S.of(context).savedViewShowOnDashboardLabel),
|
||||||
|
),
|
||||||
|
FormBuilderCheckbox(
|
||||||
|
name: fkShowInSidebar,
|
||||||
|
initialValue: false,
|
||||||
|
title: Text(S.of(context).savedViewShowInSidebarLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onCreate(BuildContext context) {
|
void _onCreate(BuildContext context) {
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
SavedView.fromDocumentFilter(
|
SavedView.fromDocumentFilter(
|
||||||
widget.currentFilter,
|
DocumentFilterForm.assembleFilter(
|
||||||
name: _formKey.currentState?.value[fkName] as String,
|
_filterFormKey,
|
||||||
|
widget.currentFilter,
|
||||||
|
),
|
||||||
|
name: _savedViewFormKey.currentState?.value[fkName] as String,
|
||||||
showOnDashboard:
|
showOnDashboard:
|
||||||
_formKey.currentState?.value[fkShowOnDashboard] as bool,
|
_savedViewFormKey.currentState?.value[fkShowOnDashboard] as bool,
|
||||||
showInSidebar: _formKey.currentState?.value[fkShowInSidebar] as bool,
|
showInSidebar:
|
||||||
|
_savedViewFormKey.currentState?.value[fkShowInSidebar] as bool,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
61
lib/features/saved_view/view/saved_view_list.dart
Normal file
61
lib/features/saved_view/view/saved_view_list.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/view/saved_view_page.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
|
class SavedViewList extends StatelessWidget {
|
||||||
|
const SavedViewList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final savedViewCubit = context.read<SavedViewCubit>();
|
||||||
|
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.value.isEmpty) {
|
||||||
|
return Text(
|
||||||
|
S.of(context).savedViewsEmptyStateText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).padded();
|
||||||
|
}
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final view = state.value.values.elementAt(index);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(view.name),
|
||||||
|
subtitle: Text(
|
||||||
|
"${view.filterRules.length} filter(s) set"), //TODO: INTL w/ placeholder
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SavedViewDetailsCubit(
|
||||||
|
context.read(),
|
||||||
|
savedView: view,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider.value(value: savedViewCubit),
|
||||||
|
],
|
||||||
|
child: SavedViewPage(
|
||||||
|
onDelete: savedViewCubit.remove,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: state.value.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
lib/features/saved_view/view/saved_view_page.dart
Normal file
133
lib/features/saved_view/view/saved_view_page.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.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';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/view_actions.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
|
class SavedViewPage extends StatefulWidget {
|
||||||
|
final Future<void> Function(SavedView savedView) onDelete;
|
||||||
|
const SavedViewPage({
|
||||||
|
super.key,
|
||||||
|
required this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SavedViewPage> createState() => _SavedViewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SavedViewPageState extends State<SavedViewPage> {
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
ViewType _viewType = ViewType.list;
|
||||||
|
SavedView get _savedView => context.read<SavedViewDetailsCubit>().savedView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_listenForLoadNewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenForLoadNewData() async {
|
||||||
|
final currState = context.read<SavedViewDetailsCubit>().state;
|
||||||
|
if (_scrollController.offset >=
|
||||||
|
_scrollController.position.maxScrollExtent * 0.7 &&
|
||||||
|
!currState.isLoading &&
|
||||||
|
!currState.isLastPageLoaded) {
|
||||||
|
try {
|
||||||
|
await context.read<SavedViewDetailsCubit>().loadMore();
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Text(_savedView.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () async {
|
||||||
|
final shouldDelete = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
ConfirmDeleteSavedViewDialog(view: _savedView),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (shouldDelete) {
|
||||||
|
await widget.onDelete(_savedView);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_viewType == ViewType.list ? Icons.grid_view_rounded : Icons.list,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _viewType = _viewType.toggle()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.hasLoaded && state.documents.isEmpty) {
|
||||||
|
return DocumentsEmptyState(state: state);
|
||||||
|
}
|
||||||
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
|
builder: (context, connectivity) {
|
||||||
|
return CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverAdaptiveDocumentsView(
|
||||||
|
documents: state.documents,
|
||||||
|
hasInternetConnection: connectivity.isConnected,
|
||||||
|
isLabelClickable: false,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
onTap: _onOpenDocumentDetails,
|
||||||
|
viewType: _viewType,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onOpenDocumentDetails(DocumentModel document) async {
|
||||||
|
final updatedDocument = await Navigator.push<DocumentModel>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => DocumentDetailsCubit(
|
||||||
|
context.read<PaperlessDocumentsApi>(),
|
||||||
|
document,
|
||||||
|
),
|
||||||
|
child: const LabelRepositoriesProvider(
|
||||||
|
child: DocumentDetailsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (updatedDocument != document) {
|
||||||
|
// Reload in case document was edited and might not fulfill filter criteria of saved view anymore
|
||||||
|
context.read<SavedViewDetailsCubit>().reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,218 +1,218 @@
|
|||||||
import 'dart:math';
|
// import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
// import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
// import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
// import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
// import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
// import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
// import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
|
// import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
// import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
// import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
// import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
// import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
// import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class SavedViewSelectionWidget extends StatelessWidget {
|
// class SavedViewSelectionWidget extends StatelessWidget {
|
||||||
final DocumentFilter currentFilter;
|
// final DocumentFilter currentFilter;
|
||||||
const SavedViewSelectionWidget({
|
// const SavedViewSelectionWidget({
|
||||||
Key? key,
|
// Key? key,
|
||||||
required this.height,
|
// required this.height,
|
||||||
required this.enabled,
|
// required this.enabled,
|
||||||
required this.currentFilter,
|
// required this.currentFilter,
|
||||||
}) : super(key: key);
|
// }) : super(key: key);
|
||||||
|
|
||||||
final double height;
|
// final double height;
|
||||||
final bool enabled;
|
// final bool enabled;
|
||||||
|
|
||||||
@override
|
// @override
|
||||||
Widget build(BuildContext context) {
|
// Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
// return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
// builder: (context, connectivityState) {
|
||||||
final hasInternetConnection = connectivityState.isConnected;
|
// final hasInternetConnection = connectivityState.isConnected;
|
||||||
return SizedBox(
|
// return SizedBox(
|
||||||
height: height,
|
// height: height,
|
||||||
child: Column(
|
// child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
// mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
// BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
builder: (context, state) {
|
// builder: (context, state) {
|
||||||
if (!state.hasLoaded) {
|
// if (!state.hasLoaded) {
|
||||||
return _buildLoadingWidget(context);
|
// return _buildLoadingWidget(context);
|
||||||
}
|
// }
|
||||||
if (state.value.isEmpty) {
|
// if (state.value.isEmpty) {
|
||||||
return Text(S.of(context).savedViewsEmptyStateText);
|
// return Text(S.of(context).savedViewsEmptyStateText);
|
||||||
}
|
// }
|
||||||
return SizedBox(
|
// return SizedBox(
|
||||||
height: 38,
|
// height: 38,
|
||||||
child: ListView.separated(
|
// child: ListView.separated(
|
||||||
itemCount: state.value.length,
|
// itemCount: state.value.length,
|
||||||
scrollDirection: Axis.horizontal,
|
// scrollDirection: Axis.horizontal,
|
||||||
itemBuilder: (context, index) {
|
// itemBuilder: (context, index) {
|
||||||
final view = state.value.values.elementAt(index);
|
// final view = state.value.values.elementAt(index);
|
||||||
return GestureDetector(
|
// return GestureDetector(
|
||||||
onLongPress: hasInternetConnection
|
// onLongPress: hasInternetConnection
|
||||||
? () => _onDelete(context, view)
|
// ? () => _onDelete(context, view)
|
||||||
: null,
|
// : null,
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, docState) {
|
// builder: (context, docState) {
|
||||||
final view = state.value.values.toList()[index];
|
// final view = state.value.values.toList()[index];
|
||||||
return FilterChip(
|
// return FilterChip(
|
||||||
label: Text(
|
// label: Text(
|
||||||
view.name,
|
// view.name,
|
||||||
),
|
// ),
|
||||||
selected:
|
// selected:
|
||||||
view.id == docState.selectedSavedViewId,
|
// view.id == docState.selectedSavedViewId,
|
||||||
onSelected: enabled && hasInternetConnection
|
// onSelected: enabled && hasInternetConnection
|
||||||
? (isSelected) =>
|
// ? (isSelected) =>
|
||||||
_onSelected(isSelected, context, view)
|
// _onSelected(isSelected, context, view)
|
||||||
: null,
|
// : null,
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
separatorBuilder: (context, index) => const SizedBox(
|
// separatorBuilder: (context, index) => const SizedBox(
|
||||||
width: 4.0,
|
// width: 4.0,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
// BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
builder: (context, state) {
|
// builder: (context, state) {
|
||||||
return Row(
|
// return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
// children: [
|
||||||
Text(
|
// Text(
|
||||||
S.of(context).savedViewsLabel,
|
// S.of(context).savedViewsLabel,
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
// style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
// ),
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
// BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
buildWhen: (previous, current) =>
|
// buildWhen: (previous, current) =>
|
||||||
previous.filter != current.filter,
|
// previous.filter != current.filter,
|
||||||
builder: (context, docState) {
|
// builder: (context, docState) {
|
||||||
return TextButton.icon(
|
// return TextButton.icon(
|
||||||
icon: const Icon(Icons.add),
|
// icon: const Icon(Icons.add),
|
||||||
onPressed: (enabled &&
|
// onPressed: (enabled &&
|
||||||
state.hasLoaded &&
|
// state.hasLoaded &&
|
||||||
hasInternetConnection)
|
// hasInternetConnection)
|
||||||
? () =>
|
// ? () =>
|
||||||
_onCreatePressed(context, docState.filter)
|
// _onCreatePressed(context, docState.filter)
|
||||||
: null,
|
// : null,
|
||||||
label: Text(S.of(context).savedViewCreateNewLabel),
|
// label: Text(S.of(context).savedViewCreateNewLabel),
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
).padded(),
|
// ).padded(),
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
Widget _buildLoadingWidget(BuildContext context) {
|
// Widget _buildLoadingWidget(BuildContext context) {
|
||||||
return SizedBox(
|
// return SizedBox(
|
||||||
height: 38,
|
// height: 38,
|
||||||
width: MediaQuery.of(context).size.width,
|
// width: MediaQuery.of(context).size.width,
|
||||||
child: Shimmer.fromColors(
|
// child: Shimmer.fromColors(
|
||||||
baseColor: Theme.of(context).brightness == Brightness.light
|
// baseColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[300]!
|
// ? Colors.grey[300]!
|
||||||
: Colors.grey[900]!,
|
// : Colors.grey[900]!,
|
||||||
highlightColor: Theme.of(context).brightness == Brightness.light
|
// highlightColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[100]!
|
// ? Colors.grey[100]!
|
||||||
: Colors.grey[600]!,
|
// : Colors.grey[600]!,
|
||||||
child: ListView(
|
// child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
// scrollDirection: Axis.horizontal,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
// physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
// children: [
|
||||||
FilterChip(
|
// FilterChip(
|
||||||
label: const SizedBox(width: 32),
|
// label: const SizedBox(width: 32),
|
||||||
onSelected: (_) {},
|
// onSelected: (_) {},
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 4.0),
|
// const SizedBox(width: 4.0),
|
||||||
FilterChip(
|
// FilterChip(
|
||||||
label: const SizedBox(width: 64),
|
// label: const SizedBox(width: 64),
|
||||||
onSelected: (_) {},
|
// onSelected: (_) {},
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 4.0),
|
// const SizedBox(width: 4.0),
|
||||||
FilterChip(
|
// FilterChip(
|
||||||
label: const SizedBox(width: 100),
|
// label: const SizedBox(width: 100),
|
||||||
onSelected: (_) {},
|
// onSelected: (_) {},
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 4.0),
|
// const SizedBox(width: 4.0),
|
||||||
FilterChip(
|
// FilterChip(
|
||||||
label: const SizedBox(width: 32),
|
// label: const SizedBox(width: 32),
|
||||||
onSelected: (_) {},
|
// onSelected: (_) {},
|
||||||
),
|
// ),
|
||||||
const SizedBox(width: 4.0),
|
// const SizedBox(width: 4.0),
|
||||||
FilterChip(
|
// FilterChip(
|
||||||
label: const SizedBox(width: 48),
|
// label: const SizedBox(width: 48),
|
||||||
onSelected: (_) {},
|
// onSelected: (_) {},
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
void _onCreatePressed(BuildContext context, DocumentFilter filter) async {
|
// void _onCreatePressed(BuildContext context, DocumentFilter filter) async {
|
||||||
final newView = await Navigator.of(context).push<SavedView?>(
|
// final newView = await Navigator.of(context).push<SavedView?>(
|
||||||
MaterialPageRoute(
|
// MaterialPageRoute(
|
||||||
builder: (context) => AddSavedViewPage(
|
// builder: (context) => AddSavedViewPage(
|
||||||
currentFilter: filter,
|
// currentFilter: filter,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
if (newView != null) {
|
// if (newView != null) {
|
||||||
try {
|
// try {
|
||||||
await context.read<SavedViewCubit>().add(newView);
|
// await context.read<SavedViewCubit>().add(newView);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
// } on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
// showErrorMessage(context, error, stackTrace);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
void _onSelected(
|
// void _onSelected(
|
||||||
bool selectionIntent,
|
// bool selectionIntent,
|
||||||
BuildContext context,
|
// BuildContext context,
|
||||||
SavedView view,
|
// SavedView view,
|
||||||
) async {
|
// ) async {
|
||||||
if (selectionIntent) {
|
// if (selectionIntent) {
|
||||||
context.read<DocumentsCubit>().selectView(view.id!);
|
// context.read<DocumentsCubit>().selectView(view.id!);
|
||||||
} else {
|
// } else {
|
||||||
context.read<DocumentsCubit>().unselectView();
|
// context.read<DocumentsCubit>().unselectView();
|
||||||
context.read<DocumentsCubit>().resetFilter();
|
// context.read<DocumentsCubit>().resetFilter();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
void _onDelete(BuildContext context, SavedView view) async {
|
// void _onDelete(BuildContext context, SavedView view) async {
|
||||||
{
|
// {
|
||||||
final delete = await showDialog<bool>(
|
// final delete = await showDialog<bool>(
|
||||||
context: context,
|
// context: context,
|
||||||
builder: (context) => ConfirmDeleteSavedViewDialog(view: view),
|
// builder: (context) => ConfirmDeleteSavedViewDialog(view: view),
|
||||||
) ??
|
// ) ??
|
||||||
false;
|
// false;
|
||||||
if (delete) {
|
// if (delete) {
|
||||||
try {
|
// try {
|
||||||
context.read<SavedViewCubit>().remove(view);
|
// context.read<SavedViewCubit>().remove(view);
|
||||||
if (context.read<DocumentsCubit>().state.selectedSavedViewId ==
|
// if (context.read<DocumentsCubit>().state.selectedSavedViewId ==
|
||||||
view.id) {
|
// view.id) {
|
||||||
await context.read<DocumentsCubit>().resetFilter();
|
// await context.read<DocumentsCubit>().resetFilter();
|
||||||
}
|
// }
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
// } on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
// showErrorMessage(context, error, stackTrace);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/search/cubit/document_search_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart';
|
import 'package:paperless_mobile/features/search/cubit/document_search_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/search/cubit/document_search_state.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
Future<void> showDocumentSearchPage(BuildContext context) {
|
Future<void> showDocumentSearchPage(BuildContext context) {
|
||||||
@@ -48,28 +45,33 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
hintStyle: theme.textTheme.bodyLarge?.apply(
|
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
hintText: "Search documents",
|
hintText: "Search documents", //TODO: INTL
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
controller: _queryController,
|
controller: _queryController,
|
||||||
onChanged: context.read<DocumentSearchCubit>().suggest,
|
onChanged: context.read<DocumentSearchCubit>().suggest,
|
||||||
onSubmitted: context.read<DocumentSearchCubit>().search,
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: (query) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
context.read<DocumentSearchCubit>().search(query);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
icon: Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<DocumentSearchCubit>().reset();
|
context.read<DocumentSearchCubit>().reset();
|
||||||
_queryController.clear();
|
_queryController.clear();
|
||||||
},
|
},
|
||||||
)
|
).padded(),
|
||||||
],
|
],
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: Size.fromHeight(1),
|
preferredSize: const Size.fromHeight(1),
|
||||||
child: Divider(
|
child: Divider(
|
||||||
color: theme.colorScheme.outline,
|
color: theme.colorScheme.outline,
|
||||||
),
|
),
|
||||||
@@ -103,7 +105,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) => ListTile(
|
(context, index) => ListTile(
|
||||||
title: Text(historyMatches[index]),
|
title: Text(historyMatches[index]),
|
||||||
leading: Icon(Icons.history),
|
leading: const Icon(Icons.history),
|
||||||
onTap: () => _selectSuggestion(historyMatches[index]),
|
onTap: () => _selectSuggestion(historyMatches[index]),
|
||||||
),
|
),
|
||||||
childCount: historyMatches.length,
|
childCount: historyMatches.length,
|
||||||
@@ -120,7 +122,7 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) => ListTile(
|
(context, index) => ListTile(
|
||||||
title: Text(suggestions[index]),
|
title: Text(suggestions[index]),
|
||||||
leading: Icon(Icons.search),
|
leading: const Icon(Icons.search),
|
||||||
onTap: () => _selectSuggestion(suggestions[index]),
|
onTap: () => _selectSuggestion(suggestions[index]),
|
||||||
),
|
),
|
||||||
childCount: suggestions.length,
|
childCount: suggestions.length,
|
||||||
@@ -135,27 +137,21 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
S.of(context).documentSearchResults,
|
S.of(context).documentSearchResults,
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
).padded();
|
).padded();
|
||||||
if (state.isLoading) {
|
|
||||||
return DocumentsListLoadingWidget(
|
|
||||||
beforeWidgets: [header],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(child: header),
|
SliverToBoxAdapter(child: header),
|
||||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
|
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
|
||||||
SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: Center(child: Text("No documents found.")),
|
child: Center(child: Text("No documents found.")), //TODO: INTL
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
SliverList(
|
SliverAdaptiveDocumentsView(
|
||||||
delegate: SliverChildBuilderDelegate(
|
documents: state.documents,
|
||||||
(context, index) => DocumentListItem(
|
hasInternetConnection: true,
|
||||||
document: state.documents[index],
|
isLabelClickable: false,
|
||||||
),
|
isLoading: state.isLoading,
|
||||||
childCount: state.documents.length,
|
hasLoaded: state.hasLoaded,
|
||||||
),
|
)
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1
lib/features/search/view/documents_search_app_bar.dart
Normal file
1
lib/features/search/view/documents_search_app_bar.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
53
lib/features/search_app_bar/view/search_app_bar.dart
Normal file
53
lib/features/search_app_bar/view/search_app_bar.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
|
typedef OpenSearchCallback = void Function(BuildContext context);
|
||||||
|
|
||||||
|
class SearchAppBar extends StatefulWidget with PreferredSizeWidget {
|
||||||
|
final PreferredSizeWidget? bottom;
|
||||||
|
final OpenSearchCallback onOpenSearch;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
const SearchAppBar({
|
||||||
|
super.key,
|
||||||
|
required this.onOpenSearch,
|
||||||
|
this.bottom,
|
||||||
|
this.backgroundColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchAppBar> createState() => _SearchAppBarState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchAppBarState extends State<SearchAppBar> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
snap: true,
|
||||||
|
backgroundColor: widget.backgroundColor,
|
||||||
|
title: SearchBar(
|
||||||
|
height: kToolbarHeight - 8,
|
||||||
|
supportingText: "Search documents",
|
||||||
|
onTap: () => widget.onOpenSearch(context),
|
||||||
|
leadingIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
Scaffold.of(context).openDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailingIcon: IconButton(
|
||||||
|
icon: const CircleAvatar(
|
||||||
|
child: Text("A"),
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
).paddedOnly(top: 4, bottom: 4),
|
||||||
|
bottom: widget.bottom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user