diff --git a/lib/core/widgets/documents_list_loading_widget.dart b/lib/core/widgets/documents_list_loading_widget.dart index 6f0f920..8d1575c 100644 --- a/lib/core/widgets/documents_list_loading_widget.dart +++ b/lib/core/widgets/documents_list_loading_widget.dart @@ -5,84 +5,95 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:shimmer/shimmer.dart'; class DocumentsListLoadingWidget extends StatelessWidget { - final List above; - final List below; - static const tags = [" ", " ", " "]; - static const titleLengths = [double.infinity, 150.0, 200.0]; - static const correspondentLengths = [200.0, 300.0, 150.0]; - static const fontSize = 16.0; + final List beforeWidgets; + final List afterWidgets; + + static const _tags = [" ", " ", " "]; + static const _titleLengths = [double.infinity, 150.0, 200.0]; + static const _correspondentLengths = [200.0, 300.0, 150.0]; + static const _fontSize = 16.0; const DocumentsListLoadingWidget({ super.key, - this.above = const [], - this.below = const [], + this.beforeWidgets = const [], + this.afterWidgets = const [], }); @override Widget build(BuildContext context) { - return ListView( - children: [ - ...above, - ...List.generate(25, (idx) { - final r = Random(idx); - final tagCount = r.nextInt(tags.length + 1); - final correspondentLength = - correspondentLengths[r.nextInt(correspondentLengths.length - 1)]; - final titleLength = titleLengths[r.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[r.nextInt(tags.length)]), - ), - ), - ).paddedOnly(top: 4), - ], - ), - ), - ), - ); - }).toList(), - ...below, + final _random = Random(); + return CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate(beforeWidgets), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return _buildFakeListItem(context, _random); + }, + ), + ), + SliverList(delegate: SliverChildListDelegate(afterWidgets)) ], ); } + + 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), + ], + ), + ), + ), + ); + } } diff --git a/lib/core/widgets/hint_card.dart b/lib/core/widgets/hint_card.dart index 27b6ddb..d32ff04 100644 --- a/lib/core/widgets/hint_card.dart +++ b/lib/core/widgets/hint_card.dart @@ -6,6 +6,7 @@ import 'package:paperless_mobile/generated/l10n.dart'; class HintCard extends StatelessWidget { final String hintText; final double elevation; + final IconData hintIcon; final VoidCallback? onHintAcknowledged; final bool show; const HintCard({ @@ -13,7 +14,8 @@ class HintCard extends StatelessWidget { required this.hintText, this.onHintAcknowledged, this.elevation = 1, - required this.show, + this.show = true, + this.hintIcon = Icons.tips_and_updates_outlined, }); @override @@ -31,7 +33,7 @@ class HintCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( - Icons.tips_and_updates_outlined, + hintIcon, color: Theme.of(context).hintColor, ).padded(), Align( @@ -52,7 +54,7 @@ class HintCard extends StatelessWidget { ), ) else - Padding(padding: EdgeInsets.only(bottom: 24)), + const Padding(padding: EdgeInsets.only(bottom: 24)), ], ).padded(), ).padded(), diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 17270e5..d8d7535 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/similar_documents_view.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -23,6 +24,7 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; +import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; import 'package:path_provider/path_provider.dart'; @@ -31,6 +33,7 @@ import 'package:badges/badges.dart' as b; import '../../../../core/repository/state/impl/document_type_repository_state.dart'; +//TODO: Refactor this into several widgets class DocumentDetailsPage extends StatefulWidget { final bool allowEdit; final bool isLabelClickable; @@ -48,6 +51,16 @@ class DocumentDetailsPage extends StatefulWidget { } class _DocumentDetailsPageState extends State { + late Future _metaData; + + @override + void initState() { + super.initState(); + _metaData = context + .read() + .getMetaData(context.read().state.document); + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -57,102 +70,11 @@ class _DocumentDetailsPageState extends State { return false; }, child: DefaultTabController( - length: 3, + length: 4, child: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - floatingActionButton: widget.allowEdit - ? BlocBuilder( - builder: (context, state) { - final _filteredSuggestions = - state.suggestions.documentDifference(state.document); - return BlocBuilder( - builder: (context, connectivityState) { - if (!connectivityState.isConnected) { - return Container(); - } - return b.Badge( - position: b.BadgePosition.topEnd(top: -12, end: -6), - showBadge: _filteredSuggestions.hasSuggestions, - child: Tooltip( - message: - S.of(context).documentDetailsPageEditTooltip, - preferBelow: false, - verticalOffset: 40, - child: FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: () => _onEdit(state.document), - ), - ), - badgeContent: Text( - '${_filteredSuggestions.suggestionsCount}', - style: const TextStyle( - color: Colors.white, - ), - ), - badgeColor: Colors.red, - ); - }, - ); - }, - ) - : null, - bottomNavigationBar: - BlocBuilder( - builder: (context, state) { - return BottomAppBar( - child: BlocBuilder( - builder: (context, connectivityState) { - final isConnected = connectivityState.isConnected; - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - tooltip: - S.of(context).documentDetailsPageDeleteTooltip, - icon: const Icon(Icons.delete), - onPressed: widget.allowEdit && isConnected - ? () => _onDelete(state.document) - : null, - ).paddedSymmetrically(horizontal: 4), - Tooltip( - message: - S.of(context).documentDetailsPageDownloadTooltip, - child: DocumentDownloadButton( - document: state.document, - enabled: isConnected, - ), - ), - IconButton( - tooltip: - S.of(context).documentDetailsPagePreviewTooltip, - icon: const Icon(Icons.visibility), - onPressed: isConnected - ? () => _onOpen(state.document) - : null, - ).paddedOnly(right: 4.0), - IconButton( - tooltip: S - .of(context) - .documentDetailsPageOpenInSystemViewerTooltip, - icon: const Icon(Icons.open_in_new), - onPressed: - isConnected ? _onOpenFileInSystemViewer : null, - ).paddedOnly(right: 4.0), - IconButton( - tooltip: - S.of(context).documentDetailsPageShareTooltip, - icon: const Icon(Icons.share), - onPressed: isConnected - ? () => _onShare(state.document) - : null, - ), - ], - ); - }, - ), - ); - }, - ), + floatingActionButton: widget.allowEdit ? _buildAppBar() : null, + bottomNavigationBar: _buildBottomAppBar(), body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverAppBar( @@ -180,6 +102,7 @@ class _DocumentDetailsPageState extends State { backgroundColor: Theme.of(context).colorScheme.primaryContainer, tabBar: TabBar( + isScrollable: true, tabs: [ Tab( child: Text( @@ -208,6 +131,18 @@ class _DocumentDetailsPageState extends State { .onPrimaryContainer), ), ), + Tab( + child: Text( + S + .of(context) + .documentDetailsPageTabSimilarDocumentsLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), ], ), ), @@ -215,19 +150,26 @@ class _DocumentDetailsPageState extends State { ], body: BlocBuilder( builder: (context, state) { - return TabBarView( - children: [ - _buildDocumentOverview( - state.document, - ), - _buildDocumentContentView( - state.document, - state, - ), - _buildDocumentMetaDataView( - state.document, - ), - ], + return BlocProvider( + create: (context) => SimilarDocumentsCubit( + context.read(), + documentId: state.document.id, + ), + child: TabBarView( + children: [ + _buildDocumentOverview( + state.document, + ), + _buildDocumentContentView( + state.document, + state, + ), + _buildDocumentMetaDataView( + state.document, + ), + _buildSimilarDocumentsView(), + ], + ), ).paddedSymmetrically(horizontal: 8); }, ), @@ -237,6 +179,94 @@ class _DocumentDetailsPageState extends State { ); } + BlocBuilder _buildAppBar() { + return BlocBuilder( + builder: (context, state) { + final _filteredSuggestions = + state.suggestions.documentDifference(state.document); + return BlocBuilder( + builder: (context, connectivityState) { + if (!connectivityState.isConnected) { + return Container(); + } + return b.Badge( + position: b.BadgePosition.topEnd(top: -12, end: -6), + showBadge: _filteredSuggestions.hasSuggestions, + child: Tooltip( + message: S.of(context).documentDetailsPageEditTooltip, + preferBelow: false, + verticalOffset: 40, + child: FloatingActionButton( + child: const Icon(Icons.edit), + onPressed: () => _onEdit(state.document), + ), + ), + badgeContent: Text( + '${_filteredSuggestions.suggestionsCount}', + style: const TextStyle( + color: Colors.white, + ), + ), + badgeColor: Colors.red, + ); + }, + ); + }, + ); + } + + BlocBuilder _buildBottomAppBar() { + return BlocBuilder( + builder: (context, state) { + return BottomAppBar( + child: BlocBuilder( + builder: (context, connectivityState) { + final isConnected = connectivityState.isConnected; + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + tooltip: S.of(context).documentDetailsPageDeleteTooltip, + icon: const Icon(Icons.delete), + onPressed: widget.allowEdit && isConnected + ? () => _onDelete(state.document) + : null, + ).paddedSymmetrically(horizontal: 4), + Tooltip( + message: S.of(context).documentDetailsPageDownloadTooltip, + child: DocumentDownloadButton( + document: state.document, + enabled: isConnected, + ), + ), + IconButton( + tooltip: S.of(context).documentDetailsPagePreviewTooltip, + icon: const Icon(Icons.visibility), + onPressed: + isConnected ? () => _onOpen(state.document) : null, + ).paddedOnly(right: 4.0), + IconButton( + tooltip: S + .of(context) + .documentDetailsPageOpenInSystemViewerTooltip, + icon: const Icon(Icons.open_in_new), + onPressed: isConnected ? _onOpenFileInSystemViewer : null, + ).paddedOnly(right: 4.0), + IconButton( + tooltip: S.of(context).documentDetailsPageShareTooltip, + icon: const Icon(Icons.share), + onPressed: + isConnected ? () => _onShare(state.document) : null, + ), + ], + ); + }, + ), + ); + }, + ); + } + Future _onEdit(DocumentModel document) async { { final cubit = context.read(); @@ -306,7 +336,7 @@ class _DocumentDetailsPageState extends State { ); } return FutureBuilder( - future: context.read().getMetaData(document), + future: _metaData, builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); @@ -465,34 +495,10 @@ class _DocumentDetailsPageState extends State { child: TagsWidget( isClickable: widget.isLabelClickable, tagIds: document.tags, - onTagSelected: (int tagId) {}, ), ), ).paddedSymmetrically(vertical: 16), ), - // _separator(), - // FutureBuilder>( - // future: getIt().findSimilar(document.id), - // builder: (context, snapshot) { - // if (!snapshot.hasData) { - // return CircularProgressIndicator(); - // } - // return ExpansionTile( - // tilePadding: const EdgeInsets.symmetric(horizontal: 8.0), - // title: Text( - // S.of(context).documentDetailsPageSimilarDocumentsLabel, - // style: - // Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold), - // ), - // children: snapshot.data! - // .map((e) => DocumentListItem( - // document: e, - // onTap: (doc) {}, - // isSelected: false, - // isAtLeastOneSelected: false)) - // .toList(), - // ); - // }), ], ); } @@ -558,6 +564,10 @@ class _DocumentDetailsPageState extends State { ' ' + suffixes[i]; } + + Widget _buildSimilarDocumentsView() { + return const SimilarDocumentsView(); + } } class _DetailsItem extends StatelessWidget { diff --git a/lib/features/document_details/view/pages/similar_documents_view.dart b/lib/features/document_details/view/pages/similar_documents_view.dart new file mode 100644 index 0000000..70e7560 --- /dev/null +++ b/lib/features/document_details/view/pages/similar_documents_view.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.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/list/document_list_item.dart'; +import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; +import 'package:paperless_mobile/util.dart'; + +class SimilarDocumentsView extends StatefulWidget { + const SimilarDocumentsView({super.key}); + + @override + State createState() => _SimilarDocumentsViewState(); +} + +class _SimilarDocumentsViewState extends State { + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_listenForLoadNewData); + try { + context.read().initialize(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + @override + void dispose() { + _scrollController.removeListener(_listenForLoadNewData); + super.dispose(); + } + + void _listenForLoadNewData() async { + final currState = context.read().state; + if (_scrollController.offset >= + _scrollController.position.maxScrollExtent * 0.75 && + !currState.isLoading && + !currState.isLastPageLoaded) { + try { + await context.read().loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + } + + @override + Widget build(BuildContext context) { + const earlyPreviewHintCard = HintCard( + hintIcon: Icons.construction, + hintText: "This view is still work in progress.", + ); + return BlocBuilder( + builder: (context, state) { + if (!state.hasLoaded) { + return const DocumentsListLoadingWidget( + beforeWidgets: [earlyPreviewHintCard], + ); + } + if (state.documents.isEmpty) { + return DocumentsEmptyState( + state: state, + onReset: () => context.read().updateFilter( + filter: DocumentFilter.initial.copyWith( + moreLike: () => + context.read().documentId, + ), + ), + ); + } + return CustomScrollView( + controller: _scrollController, + slivers: [ + const SliverToBoxAdapter(child: earlyPreviewHintCard), + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: state.documents.length, + (context, index) => DocumentListItem( + document: state.documents[index], + enableHeroAnimation: false, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 088a5bf..6306c0e 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -66,13 +66,17 @@ class _DocumentsPageState extends State { ..addListener(_listenForLoadNewData); } - void _listenForLoadNewData() { + void _listenForLoadNewData() async { final currState = context.read().state; if (_scrollController.offset >= _scrollController.position.maxScrollExtent * 0.75 && !currState.isLoading && !currState.isLastPageLoaded) { - _loadNewPage(); + try { + await context.read().loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } } } @@ -353,8 +357,6 @@ class _DocumentsPageState extends State { .equals(previous.documents, current.documents) || previous.selectedIds != current.selectedIds, builder: (context, state) { - // Some ugly tricks to make it work with bloc, update pageController - if (state.hasLoaded && state.documents.isEmpty) { return DocumentsEmptyState( state: state, @@ -491,14 +493,6 @@ class _DocumentsPageState extends State { } } - Future _loadNewPage() async { - try { - await context.read().loadMore(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - void _onSelected(DocumentModel model) { context.read().toggleDocumentSelection(model); } diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index 050507c..0d5e6cd 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/empty_state.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class DocumentsEmptyState extends StatelessWidget { - final DocumentsState state; + final DocumentsPagedState state; final VoidCallback onReset; const DocumentsEmptyState({ Key? key, diff --git a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart index f7d3267..57ba0f0 100644 --- a/lib/features/documents/view/widgets/list/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/list/adaptive_documents_view.dart @@ -64,13 +64,6 @@ class AdaptiveDocumentsView extends StatelessWidget { 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, onCorrespondentSelected: onCorrespondentSelected, onDocumentTypeSelected: onDocumentTypeSelected, diff --git a/lib/features/documents/view/widgets/list/document_list_item.dart b/lib/features/documents/view/widgets/list/document_list_item.dart index 0c0490b..116bf63 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/list/document_list_item.dart @@ -7,31 +7,32 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d class DocumentListItem extends StatelessWidget { static const _a4AspectRatio = 1 / 1.4142; final DocumentModel document; - final void Function(DocumentModel) onTap; + final void Function(DocumentModel)? onTap; final void Function(DocumentModel)? onSelected; final bool isSelected; final bool isAtLeastOneSelected; final bool isLabelClickable; - final bool Function(int tagId) isTagSelectedPredicate; 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, - required this.onTap, + this.onTap, this.onSelected, - required this.isSelected, - required this.isAtLeastOneSelected, + this.isSelected = false, + this.isAtLeastOneSelected = false, this.isLabelClickable = true, - required this.isTagSelectedPredicate, this.onTagSelected, this.onCorrespondentSelected, this.onDocumentTypeSelected, this.onStoragePathSelected, + this.enableHeroAnimation = true, }) : super(key: key); @override @@ -85,6 +86,7 @@ class DocumentListItem extends StatelessWidget { id: document.id, fit: BoxFit.cover, alignment: Alignment.topCenter, + enableHero: enableHeroAnimation, ), ), ), @@ -96,7 +98,7 @@ class DocumentListItem extends StatelessWidget { if (isAtLeastOneSelected || isSelected) { onSelected?.call(document); } else { - onTap(document); + onTap?.call(document); } } } diff --git a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart index bdba0c6..fa39d38 100644 --- a/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart +++ b/lib/features/linked_documents_preview/view/pages/linked_documents_page.dart @@ -33,7 +33,7 @@ class _LinkedDocumentsPageState extends State { style: Theme.of(context).textTheme.bodySmall, ), if (!state.isLoaded) - Expanded(child: const DocumentsListLoadingWidget()) + const Expanded(child: DocumentsListLoadingWidget()) else Expanded( child: ListView.builder( @@ -59,10 +59,6 @@ class _LinkedDocumentsPageState extends State { ), ); }, - isSelected: false, - isAtLeastOneSelected: false, - isTagSelectedPredicate: (_) => false, - onTagSelected: (int tag) {}, ); }, ), diff --git a/lib/features/paged_document_view/model/documents_paged_state.dart b/lib/features/paged_document_view/model/documents_paged_state.dart index dd9920c..71df68b 100644 --- a/lib/features/paged_document_view/model/documents_paged_state.dart +++ b/lib/features/paged_document_view/model/documents_paged_state.dart @@ -1,6 +1,10 @@ import 'package:equatable/equatable.dart'; import 'package:paperless_api/paperless_api.dart'; +/// +/// Base state for all blocs/cubits using a paged view of documents. +/// [T] is the return type of the API call. +/// abstract class DocumentsPagedState extends Equatable { final bool hasLoaded; final bool isLoading; diff --git a/lib/features/similar_documents/cubit/similar_documents_cubit.dart b/lib/features/similar_documents/cubit/similar_documents_cubit.dart new file mode 100644 index 0000000..13dd481 --- /dev/null +++ b/lib/features/similar_documents/cubit/similar_documents_cubit.dart @@ -0,0 +1,28 @@ +import 'package:bloc/bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart'; +import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart'; + +part 'similar_documents_state.dart'; + +class SimilarDocumentsCubit extends Cubit + with DocumentsPagingMixin { + final int documentId; + + @override + final PaperlessDocumentsApi api; + + SimilarDocumentsCubit( + this.api, { + required this.documentId, + }) : super(const SimilarDocumentsState()); + + Future initialize() async { + if (!state.hasLoaded) { + await updateFilter( + filter: state.filter.copyWith(moreLike: () => documentId), + ); + emit(state.copyWith(hasLoaded: true)); + } + } +} diff --git a/lib/features/similar_documents/cubit/similar_documents_state.dart b/lib/features/similar_documents/cubit/similar_documents_state.dart new file mode 100644 index 0000000..4c4c664 --- /dev/null +++ b/lib/features/similar_documents/cubit/similar_documents_state.dart @@ -0,0 +1,47 @@ +part of 'similar_documents_cubit.dart'; + +class SimilarDocumentsState extends DocumentsPagedState { + const SimilarDocumentsState({ + super.filter, + super.hasLoaded, + super.isLoading, + super.value, + }); + + @override + List get props => [ + filter, + hasLoaded, + isLoading, + value, + ]; + + @override + SimilarDocumentsState copyWithPaged({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return copyWith( + hasLoaded: hasLoaded, + isLoading: isLoading, + value: value, + filter: filter, + ); + } + + SimilarDocumentsState copyWith({ + bool? hasLoaded, + bool? isLoading, + List>? value, + DocumentFilter? filter, + }) { + return SimilarDocumentsState( + hasLoaded: hasLoaded ?? this.hasLoaded, + isLoading: isLoading ?? this.isLoading, + value: value ?? this.value, + filter: filter ?? this.filter, + ); + } +} diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 76222dc..b425bbc 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Přehled", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Typ dokumentu", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Dokument úspěšně stažen.", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 4738b2d..02b9924 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Übersicht", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Ähnliche Dokumente", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Dokumenttyp", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5243d9d..72a3ce5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Overview", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Document Type", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Document successfully downloaded.", diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 17f7ae1..61dd7c7 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -90,6 +90,8 @@ "@documentDetailsPageTabMetaDataLabel": {}, "documentDetailsPageTabOverviewLabel": "Genel bakış", "@documentDetailsPageTabOverviewLabel": {}, + "documentDetailsPageTabSimilarDocumentsLabel": "Similar Documents", + "@documentDetailsPageTabSimilarDocumentsLabel": {}, "documentDocumentTypePropertyLabel": "Döküman tipi", "@documentDocumentTypePropertyLabel": {}, "documentDownloadSuccessMessage": "Döküman başarıyla indirildi.", diff --git a/packages/paperless_api/lib/src/converters/converters.dart b/packages/paperless_api/lib/src/converters/converters.dart index 10a8c8e..8a96d0c 100644 --- a/packages/paperless_api/lib/src/converters/converters.dart +++ b/packages/paperless_api/lib/src/converters/converters.dart @@ -1,3 +1,2 @@ export 'document_model_json_converter.dart'; -export 'similar_document_model_json_converter.dart'; export 'date_range_query_json_converter.dart'; diff --git a/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart b/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart deleted file mode 100644 index 2b34c84..0000000 --- a/packages/paperless_api/lib/src/converters/similar_document_model_json_converter.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/paperless_api.dart'; - -class SimilarDocumentModelJsonConverter - extends JsonConverter> { - @override - SimilarDocumentModel fromJson(Map json) { - return SimilarDocumentModel.fromJson(json); - } - - @override - Map toJson(SimilarDocumentModel object) { - return object.toJson(); - } -} diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index 4027ca2..f612cdd 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -33,6 +33,9 @@ class DocumentFilter extends Equatable { final DateRangeQuery modified; final TextQuery query; + /// Query documents similar to the document with this id. + final int? moreLike; + const DocumentFilter({ this.documentType = const IdQueryParameter.unset(), this.correspondent = const IdQueryParameter.unset(), @@ -47,6 +50,7 @@ class DocumentFilter extends Equatable { this.added = const UnsetDateRangeQuery(), this.created = const UnsetDateRangeQuery(), this.modified = const UnsetDateRangeQuery(), + this.moreLike, }); bool get forceExtendedQuery { @@ -77,6 +81,10 @@ class DocumentFilter extends Equatable { ), ); } + + if (moreLike != null) { + params.add(MapEntry('more_like_id', moreLike.toString())); + } // Reverse ordering can also be encoded using &reverse=1 // Merge query params final queryParams = groupBy(params, (e) => e.key).map( @@ -107,7 +115,7 @@ class DocumentFilter extends Equatable { DateRangeQuery? created, DateRangeQuery? modified, TextQuery? query, - int? selectedViewId, + int? Function()? moreLike, }) { final newFilter = DocumentFilter( pageSize: pageSize ?? this.pageSize, @@ -123,6 +131,7 @@ class DocumentFilter extends Equatable { added: added ?? this.added, created: created ?? this.created, modified: modified ?? this.modified, + moreLike: moreLike != null ? moreLike.call() : this.moreLike, ); if (query?.queryType != QueryType.extended && newFilter.forceExtendedQuery) { diff --git a/packages/paperless_api/lib/src/models/document_filter.g.dart b/packages/paperless_api/lib/src/models/document_filter.g.dart index 8c0b89d..6885d77 100644 --- a/packages/paperless_api/lib/src/models/document_filter.g.dart +++ b/packages/paperless_api/lib/src/models/document_filter.g.dart @@ -48,6 +48,7 @@ DocumentFilter _$DocumentFilterFromJson(Map json) => ? const UnsetDateRangeQuery() : const DateRangeQueryJsonConverter() .fromJson(json['modified'] as Map), + moreLike: json['moreLike'] as int?, ); Map _$DocumentFilterToJson(DocumentFilter instance) => @@ -65,6 +66,7 @@ Map _$DocumentFilterToJson(DocumentFilter instance) => 'added': const DateRangeQueryJsonConverter().toJson(instance.added), 'modified': const DateRangeQueryJsonConverter().toJson(instance.modified), 'query': instance.query.toJson(), + 'moreLike': instance.moreLike, }; const _$SortFieldEnumMap = { diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index 9fad865..2c547c5 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; +import 'package:paperless_api/src/models/search_hit.dart'; part 'document_model.g.dart'; @@ -37,6 +38,12 @@ class DocumentModel extends Equatable { final String originalFileName; final String? archivedFileName; + @JsonKey( + name: '__search_hit__', + includeIfNull: false, + ) + final SearchHit? searchHit; + const DocumentModel({ required this.id, required this.title, @@ -51,6 +58,7 @@ class DocumentModel extends Equatable { required this.originalFileName, this.archivedFileName, this.storagePath, + this.searchHit, }); factory DocumentModel.fromJson(Map json) => diff --git a/packages/paperless_api/lib/src/models/document_model.g.dart b/packages/paperless_api/lib/src/models/document_model.g.dart index 90c6fa8..83df5ed 100644 --- a/packages/paperless_api/lib/src/models/document_model.g.dart +++ b/packages/paperless_api/lib/src/models/document_model.g.dart @@ -25,21 +25,34 @@ DocumentModel _$DocumentModelFromJson(Map json) => originalFileName: json['original_file_name'] as String, archivedFileName: json['archived_file_name'] as String?, storagePath: json['storage_path'] as int?, + searchHit: json['__search_hit__'] == null + ? null + : SearchHit.fromJson(json['__search_hit__'] as Map), ); -Map _$DocumentModelToJson(DocumentModel instance) => - { - 'id': instance.id, - 'title': instance.title, - 'content': instance.content, - 'tags': instance.tags.toList(), - 'document_type': instance.documentType, - 'correspondent': instance.correspondent, - 'storage_path': instance.storagePath, - 'created': const LocalDateTimeJsonConverter().toJson(instance.created), - 'modified': const LocalDateTimeJsonConverter().toJson(instance.modified), - 'added': const LocalDateTimeJsonConverter().toJson(instance.added), - 'archive_serial_number': instance.archiveSerialNumber, - 'original_file_name': instance.originalFileName, - 'archived_file_name': instance.archivedFileName, - }; +Map _$DocumentModelToJson(DocumentModel instance) { + final val = { + 'id': instance.id, + 'title': instance.title, + 'content': instance.content, + 'tags': instance.tags.toList(), + 'document_type': instance.documentType, + 'correspondent': instance.correspondent, + 'storage_path': instance.storagePath, + 'created': const LocalDateTimeJsonConverter().toJson(instance.created), + 'modified': const LocalDateTimeJsonConverter().toJson(instance.modified), + 'added': const LocalDateTimeJsonConverter().toJson(instance.added), + 'archive_serial_number': instance.archiveSerialNumber, + 'original_file_name': instance.originalFileName, + 'archived_file_name': instance.archivedFileName, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__search_hit__', instance.searchHit); + return val; +} diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index 3d43bd0..7ba46a8 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -21,7 +21,6 @@ export 'paperless_server_exception.dart'; export 'paperless_server_information_model.dart'; export 'paperless_server_statistics_model.dart'; export 'saved_view_model.dart'; -export 'similar_document_model.dart'; export 'task/task.dart'; export 'task/task_status.dart'; export 'field_suggestions.dart'; diff --git a/packages/paperless_api/lib/src/models/similar_document_model.dart b/packages/paperless_api/lib/src/models/similar_document_model.dart deleted file mode 100644 index 173b270..0000000 --- a/packages/paperless_api/lib/src/models/similar_document_model.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; -import 'package:paperless_api/src/models/document_model.dart'; -import 'package:paperless_api/src/models/search_hit.dart'; - -part 'similar_document_model.g.dart'; - -@LocalDateTimeJsonConverter() -@JsonSerializable() -class SimilarDocumentModel extends DocumentModel { - @JsonKey(name: '__search_hit__') - final SearchHit searchHit; - - const SimilarDocumentModel({ - required super.id, - required super.title, - required super.documentType, - required super.correspondent, - required super.created, - required super.modified, - required super.added, - required super.originalFileName, - required this.searchHit, - super.archiveSerialNumber, - super.archivedFileName, - super.content, - super.storagePath, - super.tags, - }); - - factory SimilarDocumentModel.fromJson(Map json) => - _$SimilarDocumentModelFromJson(json); - - @override - Map toJson() => _$SimilarDocumentModelToJson(this); -} diff --git a/packages/paperless_api/lib/src/models/similar_document_model.g.dart b/packages/paperless_api/lib/src/models/similar_document_model.g.dart deleted file mode 100644 index d2f996d..0000000 --- a/packages/paperless_api/lib/src/models/similar_document_model.g.dart +++ /dev/null @@ -1,50 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'similar_document_model.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SimilarDocumentModel _$SimilarDocumentModelFromJson( - Map json) => - SimilarDocumentModel( - id: json['id'] as int, - title: json['title'] as String, - documentType: json['documentType'] as int?, - correspondent: json['correspondent'] as int?, - created: const LocalDateTimeJsonConverter() - .fromJson(json['created'] as String), - modified: const LocalDateTimeJsonConverter() - .fromJson(json['modified'] as String), - added: - const LocalDateTimeJsonConverter().fromJson(json['added'] as String), - originalFileName: json['originalFileName'] as String, - searchHit: - SearchHit.fromJson(json['__search_hit__'] as Map), - archiveSerialNumber: json['archiveSerialNumber'] as int?, - archivedFileName: json['archivedFileName'] as String?, - content: json['content'] as String?, - storagePath: json['storagePath'] as int?, - tags: (json['tags'] as List?)?.map((e) => e as int) ?? - const [], - ); - -Map _$SimilarDocumentModelToJson( - SimilarDocumentModel instance) => - { - 'id': instance.id, - 'title': instance.title, - 'content': instance.content, - 'tags': instance.tags.toList(), - 'documentType': instance.documentType, - 'correspondent': instance.correspondent, - 'storagePath': instance.storagePath, - 'created': const LocalDateTimeJsonConverter().toJson(instance.created), - 'modified': const LocalDateTimeJsonConverter().toJson(instance.modified), - 'added': const LocalDateTimeJsonConverter().toJson(instance.added), - 'archiveSerialNumber': instance.archiveSerialNumber, - 'originalFileName': instance.originalFileName, - 'archivedFileName': instance.archivedFileName, - '__search_hit__': instance.searchHit, - }; diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart index 340469b..aabd746 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api.dart @@ -18,7 +18,6 @@ abstract class PaperlessDocumentsApi { Future findNextAsn(); Future> findAll(DocumentFilter filter); Future find(int id); - Future> findSimilar(int docId); Future delete(DocumentModel doc); Future getMetaData(DocumentModel document); Future> bulkAction(BulkAction action); diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 9b99bd7..0536ddb 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -241,27 +241,6 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } } - @override - Future> findSimilar(int docId) async { - try { - final response = - await client.get("/api/documents/?more_like=$docId&pageSize=10"); - if (response.statusCode == 200) { - return (await compute( - PagedSearchResult.fromJsonSingleParam, - PagedSearchResultJsonSerializer( - response.data, - SimilarDocumentModelJsonConverter(), - ), - )) - .results; - } - throw const PaperlessServerException(ErrorCode.similarQueryError); - } on DioError catch (err) { - throw err.error; - } - } - @override Future findSuggestions(DocumentModel document) async { try {