mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-16 00:12:31 -06:00
Improved search, changed saved view display
This commit is contained in:
@@ -66,38 +66,11 @@ class DocumentsCubit extends HydratedCubit<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 {
|
||||
final res = await api.autocomplete(query);
|
||||
return res;
|
||||
}
|
||||
|
||||
void unselectView() {
|
||||
emit(state.copyWith(selectedSavedViewId: () => null));
|
||||
}
|
||||
|
||||
@override
|
||||
DocumentsState? fromJson(Map<String, dynamic> 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';
|
||||
|
||||
class DocumentsState extends PagedDocumentsState {
|
||||
final int? selectedSavedViewId;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
final List<DocumentModel> selection;
|
||||
|
||||
const DocumentsState({
|
||||
this.selection = const [],
|
||||
this.selectedSavedViewId,
|
||||
super.value = const [],
|
||||
super.filter = const DocumentFilter(),
|
||||
super.hasLoaded = false,
|
||||
@@ -25,7 +22,6 @@ class DocumentsState extends PagedDocumentsState {
|
||||
List<PagedSearchResult<DocumentModel>>? value,
|
||||
DocumentFilter? filter,
|
||||
List<DocumentModel>? selection,
|
||||
int? Function()? selectedSavedViewId,
|
||||
}) {
|
||||
return DocumentsState(
|
||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||
@@ -33,9 +29,6 @@ class DocumentsState extends PagedDocumentsState {
|
||||
value: value ?? this.value,
|
||||
filter: filter ?? this.filter,
|
||||
selection: selection ?? this.selection,
|
||||
selectedSavedViewId: selectedSavedViewId != null
|
||||
? selectedSavedViewId.call()
|
||||
: this.selectedSavedViewId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +39,6 @@ class DocumentsState extends PagedDocumentsState {
|
||||
value,
|
||||
selection,
|
||||
isLoading,
|
||||
selectedSavedViewId,
|
||||
];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -54,7 +46,6 @@ class DocumentsState extends PagedDocumentsState {
|
||||
'hasLoaded': hasLoaded,
|
||||
'isLoading': isLoading,
|
||||
'filter': filter.toJson(),
|
||||
'selectedSavedViewId': selectedSavedViewId,
|
||||
'value':
|
||||
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
|
||||
};
|
||||
@@ -65,7 +56,6 @@ class DocumentsState extends PagedDocumentsState {
|
||||
return DocumentsState(
|
||||
hasLoaded: json['hasLoaded'],
|
||||
isLoading: json['isLoading'],
|
||||
selectedSavedViewId: json['selectedSavedViewId'],
|
||||
value: (json['value'] as List<dynamic>)
|
||||
.map((e) =>
|
||||
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:badges/badges.dart' as b;
|
||||
import 'package:collection/collection.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_mobile/core/bloc/connectivity_cubit.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/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_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/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/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/sort_documents_button.dart';
|
||||
import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/view_actions.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/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_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_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/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
class DocumentFilterIntent {
|
||||
@@ -46,6 +39,7 @@ class DocumentFilterIntent {
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Refactor this
|
||||
class DocumentsPage extends StatefulWidget {
|
||||
const DocumentsPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -53,56 +47,38 @@ class DocumentsPage extends StatefulWidget {
|
||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||
}
|
||||
|
||||
class _DocumentsPageState extends State<DocumentsPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
double _offset = 0;
|
||||
double _last = 0;
|
||||
class _DocumentsPageState extends State<DocumentsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
static const double _savedViewWidgetHeight = 80 + 16;
|
||||
int _currentTab = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(
|
||||
length: 2,
|
||||
vsync: this,
|
||||
initialIndex: 0,
|
||||
);
|
||||
try {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
context.read<SavedViewCubit>().reload();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
_scrollController
|
||||
..addListener(_listenForScrollChanges)
|
||||
..addListener(_listenForLoadNewData);
|
||||
_tabController.addListener(_listenForTabChanges);
|
||||
}
|
||||
|
||||
void _listenForLoadNewData() async {
|
||||
final currState = context.read<DocumentsCubit>().state;
|
||||
if (_scrollController.offset >=
|
||||
_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(() {});
|
||||
}
|
||||
void _listenForTabChanges() {
|
||||
setState(() {
|
||||
_currentTab = _tabController.index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -140,149 +116,204 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
return Scaffold(
|
||||
// appBar: PreferredSize(
|
||||
// preferredSize: const Size.fromHeight(
|
||||
// kToolbarHeight,
|
||||
// ),
|
||||
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
// builder: (context, state) {
|
||||
// if (state.selection.isEmpty) {
|
||||
// return DocumentSearchBar();
|
||||
// // return AppBar(
|
||||
// // title: Text(S.of(context).documentsPageTitle +
|
||||
// // " (${formatMaxCount(state.documents.length)})"),
|
||||
// // actions: [
|
||||
// // IconButton(
|
||||
// // icon: const Icon(Icons.search),
|
||||
// // onPressed: () {
|
||||
// // showMaterial3Search(
|
||||
// // context: context,
|
||||
// // delegate: DocumentSearchDelegate(
|
||||
// // DocumentSearchCubit(context.read()),
|
||||
// // searchFieldStyle:
|
||||
// // Theme.of(context).textTheme.bodyLarge,
|
||||
// // hintText: "Search documents", //TODO: INTL
|
||||
// // ),
|
||||
// // );
|
||||
// // },
|
||||
// // ),
|
||||
// // 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,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
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: _currentTab == 0
|
||||
? FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
)
|
||||
: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () => _onCreateSavedView(state.filter),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
snap: true,
|
||||
title: SearchBar(
|
||||
height: kToolbarHeight - 2,
|
||||
supportingText: "Search documents",
|
||||
onTap: () {
|
||||
showDocumentSearchPage(context);
|
||||
},
|
||||
leadingIcon: Icon(Icons.menu),
|
||||
trailingIcon: CircleAvatar(
|
||||
child: Text("A"),
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||
// and redirects it to the SliverOverlapInjector below. If it is
|
||||
// missing, then it is possible for the nested "inner" scroll view
|
||||
// below to end up under the SliverAppBar even when the inner
|
||||
// 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: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context
|
||||
.read<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||
builder: (context, taskState) {
|
||||
return _buildBody(connectivityState);
|
||||
// return Stack(
|
||||
// children: [
|
||||
// Positioned(
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// top: _offset,
|
||||
// child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
// builder: (context, state) {
|
||||
// return ColoredBox(
|
||||
// color:
|
||||
// Theme.of(context).colorScheme.background,
|
||||
// child: SavedViewSelectionWidget(
|
||||
// height: _savedViewWidgetHeight,
|
||||
// currentFilter: state.filter,
|
||||
// enabled: state.selection.isEmpty &&
|
||||
// connectivityState.isConnected,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
),
|
||||
],
|
||||
body: NotificationListener<ScrollUpdateNotification>(
|
||||
onNotification: (notification) {
|
||||
final metrics = notification.metrics;
|
||||
final desiredTab =
|
||||
(metrics.pixels / metrics.maxScrollExtent).round();
|
||||
if (metrics.axis == Axis.horizontal &&
|
||||
_currentTab != desiredTab) {
|
||||
setState(() => _currentTab = desiredTab);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: NotificationListener<ScrollMetricsNotification>(
|
||||
onNotification: (notification) {
|
||||
// Listen for scroll notifications to load new data.
|
||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||
final currState = context.read<DocumentsCubit>().state;
|
||||
final max = notification.metrics.maxScrollExtent;
|
||||
if (max == 0 ||
|
||||
_currentTab != 0 ||
|
||||
currState.isLoading ||
|
||||
currState.isLastPageLoaded) {
|
||||
return true;
|
||||
}
|
||||
final offset = notification.metrics.pixels;
|
||||
if (offset >= max * 0.7) {
|
||||
context
|
||||
.read<DocumentsCubit>()
|
||||
.loadMore()
|
||||
.onError<PaperlessServerException>(
|
||||
(error, stackTrace) => showErrorMessage(
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
);
|
||||
}
|
||||
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>
|
||||
_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());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: Add app bar...
|
||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
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 {
|
||||
final draggableSheetController = DraggableScrollableController();
|
||||
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
||||
@@ -373,12 +402,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
try {
|
||||
if (filterIntent.shouldReset) {
|
||||
await context.read<DocumentsCubit>().resetFilter();
|
||||
context.read<DocumentsCubit>().unselectView();
|
||||
} else {
|
||||
if (filterIntent.filter !=
|
||||
context.read<DocumentsCubit>().state.filter) {
|
||||
context.read<DocumentsCubit>().unselectView();
|
||||
}
|
||||
await context
|
||||
.read<DocumentsCubit>()
|
||||
.updateFilter(filter: filterIntent.filter!);
|
||||
@@ -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 {
|
||||
final potentiallyUpdatedModel =
|
||||
await Navigator.of(context).push<DocumentModel?>(
|
||||
final updatedModel = await Navigator.of(context).push<DocumentModel?>(
|
||||
_buildDetailsPageRoute(document),
|
||||
);
|
||||
if (potentiallyUpdatedModel != document) {
|
||||
if (updatedModel != document) {
|
||||
context.read<DocumentsCubit>().reload();
|
||||
}
|
||||
}
|
||||
@@ -558,15 +518,19 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelected(DocumentModel model) {
|
||||
context.read<DocumentsCubit>().toggleDocumentSelection(model);
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() async {
|
||||
Future<void> _onReloadDocuments() async {
|
||||
try {
|
||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||
context.read<DocumentsCubit>().reload();
|
||||
context.read<SavedViewCubit>().reload();
|
||||
await context.read<DocumentsCubit>().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) {
|
||||
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 {
|
||||
final PagedDocumentsState state;
|
||||
final VoidCallback onReset;
|
||||
final VoidCallback? onReset;
|
||||
const DocumentsEmptyState({
|
||||
Key? key,
|
||||
required this.state,
|
||||
required this.onReset,
|
||||
this.onReset,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -20,7 +20,7 @@ class DocumentsEmptyState extends StatelessWidget {
|
||||
child: EmptyState(
|
||||
title: S.of(context).documentsPageEmptyStateOopsText,
|
||||
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
|
||||
bottomChild: state.filter != DocumentFilter.initial
|
||||
bottomChild: state.filter != DocumentFilter.initial && onReset != null
|
||||
? TextButton(
|
||||
onPressed: onReset,
|
||||
child: Text(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
static const _tags = [" ", " ", " "];
|
||||
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
||||
static const _correspondentLengths = <double>[200.0, 300.0, 150.0];
|
||||
static const _fontSize = 16.0;
|
||||
|
||||
const DocumentsListLoadingWidget({super.key
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _random = Random();
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return _buildFakeListItem(context, _random);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFakeListItem(BuildContext context, Random random) {
|
||||
final tagCount = random.nextInt(_tags.length + 1);
|
||||
final correspondentLength =
|
||||
_correspondentLengths[random.nextInt(_correspondentLengths.length - 1)];
|
||||
final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)];
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[300]!
|
||||
: Colors.grey[900]!,
|
||||
highlightColor: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]!
|
||||
: Colors.grey[600]!,
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
dense: true,
|
||||
isThreeLine: true,
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 50,
|
||||
width: 35,
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
width: correspondentLength,
|
||||
height: _fontSize,
|
||||
color: Colors.white,
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
height: _fontSize,
|
||||
width: titleLength,
|
||||
color: Colors.white,
|
||||
),
|
||||
Wrap(
|
||||
spacing: 2.0,
|
||||
children: List.generate(
|
||||
tagCount,
|
||||
(index) => InputChip(
|
||||
label: Text(_tags[random.nextInt(_tags.length)]),
|
||||
),
|
||||
),
|
||||
).paddedOnly(top: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,35 @@
|
||||
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/items/document_item.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/tags/view/widgets/tags_widget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DocumentGridItem extends StatelessWidget {
|
||||
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;
|
||||
|
||||
class DocumentGridItem extends DocumentItem {
|
||||
const DocumentGridItem({
|
||||
Key? key,
|
||||
required this.document,
|
||||
required this.onTap,
|
||||
required this.onSelected,
|
||||
required this.isSelected,
|
||||
required this.isAtLeastOneSelected,
|
||||
required this.isTagSelectedPredicate,
|
||||
required this.onTagSelected,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
required super.document,
|
||||
required super.isSelected,
|
||||
required super.isSelectionActive,
|
||||
required super.isLabelClickable,
|
||||
super.onCorrespondentSelected,
|
||||
super.onDocumentTypeSelected,
|
||||
super.onSelected,
|
||||
super.onStoragePathSelected,
|
||||
super.onTagSelected,
|
||||
super.onTap,
|
||||
required super.enableHeroAnimation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
onLongPress: () => onSelected(document),
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
child: AbsorbPointer(
|
||||
absorbing: isAtLeastOneSelected,
|
||||
absorbing: isSelectionActive,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
@@ -48,6 +45,7 @@ class DocumentGridItem extends StatelessWidget {
|
||||
child: DocumentPreview(
|
||||
id: document.id,
|
||||
borderRadius: 12.0,
|
||||
enableHero: enableHeroAnimation,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -94,10 +92,10 @@ class DocumentGridItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (isAtLeastOneSelected || isSelected) {
|
||||
onSelected(document);
|
||||
if (isSelectionActive || isSelected) {
|
||||
onSelected?.call(document);
|
||||
} 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:paperless_api/paperless_api.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/tags/view/widgets/tags_widget.dart';
|
||||
|
||||
class DocumentListItem extends StatelessWidget {
|
||||
class DocumentListItem extends DocumentItem {
|
||||
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({
|
||||
Key? key,
|
||||
required this.document,
|
||||
this.onTap,
|
||||
this.onSelected,
|
||||
this.isSelected = false,
|
||||
this.isAtLeastOneSelected = false,
|
||||
this.isLabelClickable = true,
|
||||
this.onTagSelected,
|
||||
this.onCorrespondentSelected,
|
||||
this.onDocumentTypeSelected,
|
||||
this.onStoragePathSelected,
|
||||
this.enableHeroAnimation = true,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
required super.document,
|
||||
required super.isSelected,
|
||||
required super.isSelectionActive,
|
||||
required super.isLabelClickable,
|
||||
super.onCorrespondentSelected,
|
||||
super.onDocumentTypeSelected,
|
||||
super.onSelected,
|
||||
super.onStoragePathSelected,
|
||||
super.onTagSelected,
|
||||
super.onTap,
|
||||
super.enableHeroAnimation = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -50,7 +37,7 @@ class DocumentListItem extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
AbsorbPointer(
|
||||
absorbing: isAtLeastOneSelected,
|
||||
absorbing: isSelectionActive,
|
||||
child: CorrespondentWidget(
|
||||
isClickable: isLabelClickable,
|
||||
correspondentId: document.correspondent,
|
||||
@@ -69,7 +56,7 @@ class DocumentListItem extends StatelessWidget {
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: AbsorbPointer(
|
||||
absorbing: isAtLeastOneSelected,
|
||||
absorbing: isSelectionActive,
|
||||
child: TagsWidget(
|
||||
isClickable: isLabelClickable,
|
||||
tagIds: document.tags,
|
||||
@@ -95,7 +82,7 @@ class DocumentListItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (isAtLeastOneSelected || isSelected) {
|
||||
if (isSelectionActive || isSelected) {
|
||||
onSelected?.call(document);
|
||||
} else {
|
||||
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/extensions/flutter_extensions.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/labels/bloc/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||
@@ -32,22 +33,14 @@ class DocumentFilterPanel extends StatefulWidget {
|
||||
}
|
||||
|
||||
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>();
|
||||
late bool _allowOnlyExtendedQuery;
|
||||
|
||||
double _heightAnimationValue = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
|
||||
|
||||
widget.draggableSheetController.addListener(animateTitleByDrag);
|
||||
}
|
||||
|
||||
@@ -106,100 +99,59 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||
),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: _buildFormList(context),
|
||||
body: DocumentFilterForm(
|
||||
formKey: _formKey,
|
||||
scrollController: widget.scrollController,
|
||||
initialFilter: widget.initialFilter,
|
||||
header: _buildPanelHeader(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormList(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
controller: widget.scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: kToolbarHeight + 22,
|
||||
title: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 1 - _heightAnimationValue,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
Widget _buildPanelHeader() {
|
||||
return SliverAppBar(
|
||||
pinned: true,
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: kToolbarHeight + 22,
|
||||
title: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 1 - _heightAnimationValue,
|
||||
child: Padding(
|
||||
padding: const 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
..._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() {
|
||||
return Container(
|
||||
// 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 {
|
||||
FocusScope.of(context).unfocus();
|
||||
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 {
|
||||
_formKey.currentState?.save();
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
DocumentFilter newFilter = _assembleFilter();
|
||||
DocumentFilter newFilter =
|
||||
DocumentFilterForm.assembleFilter(_formKey, widget.initialFilter);
|
||||
FocusScope.of(context).unfocus();
|
||||
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()),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user