From 6f66bf27fd46dc21546ca2b3885622593bb6bcaf Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Tue, 14 Feb 2023 00:24:14 +0100 Subject: [PATCH] feat: Implemented new view type, fix connectivity issues, fix offline issues, fix loading animations, fix documents page paging --- lib/core/security/session_manager.dart | 11 +- lib/extensions/flutter_extensions.dart | 23 +- .../view/pages/document_details_page.dart | 205 +++++---- .../widgets/document_meta_data_widget.dart | 15 +- .../documents/view/pages/document_view.dart | 5 +- .../documents/view/pages/documents_page.dart | 419 ++++++++++-------- .../view/widgets/adaptive_documents_view.dart | 30 +- .../view/widgets/document_preview.dart | 52 ++- .../widgets/items/document_detailed_item.dart | 123 ++++- .../widgets/items/document_grid_item.dart | 112 +++-- .../widgets/items/document_list_item.dart | 2 +- .../document_grid_loading_widget.dart | 43 +- .../document_item_placeholder.dart | 30 -- .../documents_list_loading_widget.dart | 39 +- .../widgets/placeholder/tags_placeholder.dart | 1 + .../widgets/placeholder/text_placeholder.dart | 5 - .../selection/view_type_selection_widget.dart | 42 +- lib/features/home/view/home_page.dart | 40 +- lib/features/inbox/view/pages/inbox_page.dart | 5 +- .../inbox/view/widgets/inbox_item.dart | 2 +- .../view/widgets/document_type_widget.dart | 2 + .../view/linked_documents_page.dart | 2 +- .../view/document_paging_view_mixin.dart | 6 +- .../saved_view/view/saved_view_list.dart | 82 ++-- .../view/saved_view_details_page.dart | 4 +- .../search_app_bar/view/search_app_bar.dart | 20 +- .../view/dialogs/account_settings_dialog.dart | 5 +- .../view/similar_documents_view.dart | 83 ++-- .../paperless_server_information_model.dart | 4 + 29 files changed, 806 insertions(+), 606 deletions(-) rename lib/features/documents/view/widgets/{ => placeholder}/document_grid_loading_widget.dart (69%) delete mode 100644 lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart rename lib/features/documents/view/widgets/{ => placeholder}/documents_list_loading_widget.dart (65%) diff --git a/lib/core/security/session_manager.dart b/lib/core/security/session_manager.dart index 8cbc96d..37c0c58 100644 --- a/lib/core/security/session_manager.dart +++ b/lib/core/security/session_manager.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; @@ -18,7 +19,9 @@ class SessionManager { static Dio _initDio(List interceptors) { //en- and decoded by utf8 by default - final Dio dio = Dio(BaseOptions()); + final Dio dio = Dio( + BaseOptions(contentType: Headers.jsonContentType), + ); dio.options.receiveTimeout = const Duration(seconds: 25); dio.options.responseType = ResponseType.json; (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = @@ -71,7 +74,9 @@ class SessionManager { } if (authToken != null) { - client.options.headers.addAll({'Authorization': 'Token $authToken'}); + client.options.headers.addAll({ + HttpHeaders.authorizationHeader: 'Token $authToken', + }); } if (serverInformation != null) { @@ -82,7 +87,7 @@ class SessionManager { void resetSettings() { client.httpClientAdapter = IOHttpClientAdapter(); client.options.baseUrl = ''; - client.options.headers.remove('Authorization'); + client.options.headers.remove(HttpHeaders.authorizationHeader); serverInformation = PaperlessServerInformationModel(); } } diff --git a/lib/extensions/flutter_extensions.dart b/lib/extensions/flutter_extensions.dart index d599d11..a4cd2e9 100644 --- a/lib/extensions/flutter_extensions.dart +++ b/lib/extensions/flutter_extensions.dart @@ -8,18 +8,22 @@ extension WidgetPadding on Widget { ); } - Widget paddedSymmetrically({double horizontal = 0.0, double vertical = 0.0}) { + Widget paddedSymmetrically({ + double horizontal = 0.0, + double vertical = 0.0, + }) { return Padding( padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), child: this, ); } - Widget paddedOnly( - {double top = 0.0, - double bottom = 0.0, - double left = 0.0, - double right = 0.0}) { + Widget paddedOnly({ + double top = 0.0, + double bottom = 0.0, + double left = 0.0, + double right = 0.0, + }) { return Padding( padding: EdgeInsets.only( top: top, @@ -30,6 +34,13 @@ extension WidgetPadding on Widget { child: this, ); } + + Widget paddedLTRB(double left, double top, double right, double bottom) { + return Padding( + padding: EdgeInsets.fromLTRB(left, top, right, bottom), + child: this, + ); + } } extension WidgetsPadding on List { 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 4661920..6678ed2 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -49,6 +49,10 @@ class _DocumentDetailsPageState extends State { @override void initState() { super.initState(); + _loadMetaData(); + } + + void _loadMetaData() { _metaData = context .read() .getMetaData(context.read().state.document); @@ -64,108 +68,117 @@ class _DocumentDetailsPageState extends State { }, child: DefaultTabController( length: 4, - child: Scaffold( - floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - floatingActionButton: widget.allowEdit ? _buildEditButton() : null, - bottomNavigationBar: _buildBottomAppBar(), - body: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverAppBar( - leading: const BackButton(), - floating: true, - pinned: true, - expandedHeight: 200.0, - flexibleSpace: - BlocBuilder( - builder: (context, state) => DocumentPreview( - id: state.document.id, - fit: BoxFit.cover, + child: BlocListener( + listenWhen: (previous, current) => + !previous.isConnected && current.isConnected, + listener: (context, state) { + _loadMetaData(); + setState(() {}); + }, + child: Scaffold( + floatingActionButtonLocation: + FloatingActionButtonLocation.endDocked, + floatingActionButton: widget.allowEdit ? _buildEditButton() : null, + bottomNavigationBar: _buildBottomAppBar(), + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverAppBar( + leading: const BackButton(), + floating: true, + pinned: true, + expandedHeight: 200.0, + flexibleSpace: + BlocBuilder( + builder: (context, state) => DocumentPreview( + document: state.document, + fit: BoxFit.cover, + ), + ), + bottom: ColoredTabBar( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + tabBar: TabBar( + isScrollable: true, + tabs: [ + Tab( + child: Text( + S.of(context).documentDetailsPageTabOverviewLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + Tab( + child: Text( + S.of(context).documentDetailsPageTabContentLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + Tab( + child: Text( + S.of(context).documentDetailsPageTabMetaDataLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + Tab( + child: Text( + S + .of(context) + .documentDetailsPageTabSimilarDocumentsLabel, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + ], + ), ), ), - bottom: ColoredTabBar( - backgroundColor: - Theme.of(context).colorScheme.primaryContainer, - tabBar: TabBar( - isScrollable: true, - tabs: [ - Tab( - child: Text( - S.of(context).documentDetailsPageTabOverviewLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + ], + body: BlocBuilder( + builder: (context, state) { + return BlocProvider( + create: (context) => SimilarDocumentsCubit( + context.read(), + context.read(), + documentId: state.document.id, + ), + child: TabBarView( + children: [ + DocumentOverviewWidget( + document: state.document, + itemSpacing: _itemPadding, + queryString: widget.titleAndContentQueryString, ), - ), - Tab( - child: Text( - S.of(context).documentDetailsPageTabContentLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + DocumentContentWidget( + isFullContentLoaded: state.isFullContentLoaded, + document: state.document, + fullContent: state.fullContent, + queryString: widget.titleAndContentQueryString, ), - ), - Tab( - child: Text( - S.of(context).documentDetailsPageTabMetaDataLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + DocumentMetaDataWidget( + document: state.document, + itemSpacing: _itemPadding, + metaData: _metaData, ), - ), - Tab( - child: Text( - S - .of(context) - .documentDetailsPageTabSimilarDocumentsLabel, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ), - ], - ), - ), + const SimilarDocumentsView(), + ], + ), + ); + }, ), - ], - body: BlocBuilder( - builder: (context, state) { - return BlocProvider( - create: (context) => SimilarDocumentsCubit( - context.read(), - context.read(), - documentId: state.document.id, - ), - child: TabBarView( - children: [ - DocumentOverviewWidget( - document: state.document, - itemSpacing: _itemPadding, - queryString: widget.titleAndContentQueryString, - ), - DocumentContentWidget( - isFullContentLoaded: state.isFullContentLoaded, - document: state.document, - fullContent: state.fullContent, - queryString: widget.titleAndContentQueryString, - ), - DocumentMetaDataWidget( - document: state.document, - itemSpacing: _itemPadding, - metaData: _metaData, - ), - const SimilarDocumentsView(), - ], - ), - ); - }, ), ), ), diff --git a/lib/features/document_details/view/widgets/document_meta_data_widget.dart b/lib/features/document_details/view/widgets/document_meta_data_widget.dart index 70c11ec..2d3def8 100644 --- a/lib/features/document_details/view/widgets/document_meta_data_widget.dart +++ b/lib/features/document_details/view/widgets/document_meta_data_widget.dart @@ -25,18 +25,17 @@ class DocumentMetaDataWidget extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { - if (!state.isConnected) { - return const Center( - child: OfflineWidget(), - ); - } + builder: (context, connectivity) { return FutureBuilder( future: metaData, builder: (context, snapshot) { + if (!connectivity.isConnected && !snapshot.hasData) { + return OfflineWidget(); + } if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } + final meta = snapshot.data!; return ListView( padding: const EdgeInsets.symmetric( @@ -55,7 +54,9 @@ class DocumentMetaDataWidget extends StatelessWidget { label: Text(S .of(context) .documentDetailsPageAssignAsnButtonLabel), - onPressed: () => _assignAsn(context), + onPressed: connectivity.isConnected + ? () => _assignAsn(context) + : null, ), ).paddedOnly(bottom: itemSpacing), DetailsItem.text(DateFormat().format(document.modified), diff --git a/lib/features/documents/view/pages/document_view.dart b/lib/features/documents/view/pages/document_view.dart index f4a3348..e15d21e 100644 --- a/lib/features/documents/view/pages/document_view.dart +++ b/lib/features/documents/view/pages/document_view.dart @@ -36,11 +36,14 @@ class _DocumentViewState extends State { ), body: PdfView( builders: PdfViewBuilders( - options: const DefaultBuilderOptions(), + options: const DefaultBuilderOptions( + loaderSwitchDuration: Duration(milliseconds: 500), + ), pageLoaderBuilder: (context) => const Center( child: CircularProgressIndicator(), ), ), + scrollDirection: Axis.vertical, controller: _pdfController, ), ); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index e2a5aae..03664d6 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_ import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart'; +import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.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'; @@ -42,10 +43,18 @@ class DocumentsPage extends StatefulWidget { } class _DocumentsPageState extends State - with SingleTickerProviderStateMixin { + with + SingleTickerProviderStateMixin, + DocumentPagingViewMixin { late final TabController _tabController; + @override + ScrollController get pagingScrollController => + _nestedScrollViewKey.currentState?.innerController ?? ScrollController(); + + final GlobalKey _nestedScrollViewKey = GlobalKey(); int _currentTab = 0; + bool _showBackToTopButton = false; @override void initState() { @@ -53,21 +62,40 @@ class _DocumentsPageState extends State _tabController = TabController( length: 2, vsync: this, - initialIndex: 0, ); - try { - context.read().reload(); - context.read().reload(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - _tabController.addListener(_listenForTabChanges); + Future.wait([ + context.read().reload(), + context.read().reload(), + ]).onError( + (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + return []; + }, + ); + + _tabController.addListener(_tabChangesListener); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _nestedScrollViewKey.currentState!.innerController + ..addListener(_scrollExtentListener) + ..addListener(shouldLoadMoreDocumentsListener); + }); } - void _listenForTabChanges() { - setState(() { - _currentTab = _tabController.index; - }); + void _tabChangesListener() { + setState(() => _currentTab = _tabController.index); + } + + void _scrollExtentListener() { + if (pagingScrollController.position.pixels > + MediaQuery.of(context).size.height) { + if (!_showBackToTopButton) { + setState(() => _showBackToTopButton = true); + } + } else { + if (_showBackToTopButton) { + setState(() => _showBackToTopButton = false); + } + } } @override @@ -145,186 +173,100 @@ class _DocumentsPageState extends State } return false; }, - child: NestedScrollView( - 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: BlocBuilder( - builder: (context, state) { - if (state.selection.isNotEmpty) { - return SliverAppBar( - floating: false, - pinned: true, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => context - .read() - .resetSelection(), - ), - title: Text( - "${state.selection.length} ${S.of(context).documentsSelectedText}", - ), - actions: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _onDelete(state), + child: Stack( + children: [ + NestedScrollView( + key: _nestedScrollViewKey, + 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: BlocBuilder( + builder: (context, state) { + if (state.selection.isNotEmpty) { + return SliverAppBar( + floating: false, + pinned: true, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => context + .read() + .resetSelection(), + ), + title: Text( + "${state.selection.length} ${S.of(context).documentsSelectedText}", + ), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _onDelete(state), + ), + ], + ); + } + return SearchAppBar( + hintText: + S.of(context).documentSearchSearchDocuments, + onOpenSearch: showDocumentSearchPage, + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: S.of(context).documentsPageTitle), + Tab(text: S.of(context).savedViewsLabel), + ], ), - ], - ); + ); + }, + ), + ), + ], + body: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; } - return SearchAppBar( - hintText: S.of(context).documentSearchSearchDocuments, - onOpenSearch: showDocumentSearchPage, - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: S.of(context).documentsPageTitle), - Tab(text: S.of(context).savedViewsLabel), - ], - ), - ); + final desiredTab = + (metrics.pixels / metrics.maxScrollExtent).round(); + if (metrics.axis == Axis.horizontal && + _currentTab != desiredTab) { + setState(() => _currentTab = desiredTab); + } + return false; }, - ), - ), - ], - body: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.maxScrollExtent == 0) { - return true; - } - final desiredTab = - (metrics.pixels / metrics.maxScrollExtent).round(); - if (metrics.axis == Axis.horizontal && - _currentTab != desiredTab) { - setState(() => _currentTab = desiredTab); - } - return false; - }, - child: NotificationListener( - onNotification: (notification) { - // Listen for scroll notifications to load new data. - // Scroll controller does not work here due to nestedscrollview limitations. - final currState = context.read().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() - .loadMore() - .onError( - (error, stackTrace) => showErrorMessage( + child: TabBarView( + controller: _tabController, + children: [ + Builder( + builder: (context) { + return _buildDocumentsTab( + connectivityState, context, - error, - stackTrace, - ), - ); - } - return false; - }, - child: TabBarView( - controller: _tabController, - children: [ - Builder( - builder: (context) { - return RefreshIndicator( - edgeOffset: kToolbarHeight + kTextTabBarHeight, - onRefresh: _onReloadDocuments, - notificationPredicate: (_) => - connectivityState.isConnected, - child: CustomScrollView( - key: const PageStorageKey("documents"), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor( - context), - ), - _buildViewActions(), - BlocBuilder( - builder: (context, state) { - if (state.hasLoaded && - state.documents.isEmpty) { - return SliverToBoxAdapter( - child: DocumentsEmptyState( - state: state, - onReset: () { - context - .read() - .resetFilter(); - }, - ), - ); - } - - return SliverAdaptiveDocumentsView( - viewType: state.viewType, - onTap: _openDetails, - onSelected: context - .read() - .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 + kTextTabBarHeight, - onRefresh: _onReloadSavedViews, - notificationPredicate: (_) => - connectivityState.isConnected, - child: CustomScrollView( - key: const PageStorageKey("savedViews"), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor( - context), - ), - const SavedViewList(), - ], - ), - ); - }, - ), - ], + ); + }, + ), + Builder( + builder: (context) { + return _buildSavedViewsTab( + connectivityState, + context, + ); + }, + ), + ], + ), ), ), - ), + if (_showBackToTopButton) _buildBackToTopAction(context), + ], ), ), ); @@ -333,6 +275,109 @@ class _DocumentsPageState extends State ); } + Widget _buildBackToTopAction(BuildContext context) { + return Transform.translate( + offset: const Offset(24, -24), + child: Align( + alignment: Alignment.bottomLeft, + child: ActionChip( + backgroundColor: Theme.of(context).colorScheme.primary, + side: BorderSide.none, + avatar: Icon( + Icons.expand_less, + color: Theme.of(context).colorScheme.onPrimary, + ), + onPressed: () async { + await pagingScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInExpo, + ); + _nestedScrollViewKey.currentState?.outerController.jumpTo(0); + }, + label: Text( + "Go to top", //TODO: INTL + style: DefaultTextStyle.of(context).style.apply( + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + ), + ); + } + + Widget _buildSavedViewsTab( + ConnectivityState connectivityState, + BuildContext context, + ) { + return RefreshIndicator( + edgeOffset: kToolbarHeight + kTextTabBarHeight, + onRefresh: _onReloadSavedViews, + notificationPredicate: (_) => connectivityState.isConnected, + child: CustomScrollView( + key: const PageStorageKey("savedViews"), + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + const SavedViewList(), + ], + ), + ); + } + + Widget _buildDocumentsTab( + ConnectivityState connectivityState, + BuildContext context, + ) { + return RefreshIndicator( + edgeOffset: kToolbarHeight + kTextTabBarHeight, + onRefresh: _onReloadDocuments, + notificationPredicate: (_) => connectivityState.isConnected, + child: CustomScrollView( + key: const PageStorageKey("documents"), + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + _buildViewActions(), + BlocBuilder( + builder: (context, state) { + if (state.hasLoaded && state.documents.isEmpty) { + return SliverToBoxAdapter( + child: DocumentsEmptyState( + state: state, + onReset: context.read().resetFilter, + ), + ); + } + + return SliverAdaptiveDocumentsView( + viewType: state.viewType, + onTap: _openDetails, + onSelected: + context.read().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, + ); + }, + ), + ], + ), + ); + } + Widget _buildViewActions() { return SliverToBoxAdapter( child: Row( diff --git a/lib/features/documents/view/widgets/adaptive_documents_view.dart b/lib/features/documents/view/widgets/adaptive_documents_view.dart index 0a6ea83..e8c200a 100644 --- a/lib/features/documents/view/widgets/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -1,11 +1,12 @@ 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/document_grid_loading_widget.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.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/documents/view/widgets/placeholder/documents_list_loading_widget.dart'; +import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; abstract class AdaptiveDocumentsView extends StatelessWidget { @@ -42,6 +43,24 @@ abstract class AdaptiveDocumentsView extends StatelessWidget { required this.hasLoaded, this.enableHeroAnimation = true, }); + + AdaptiveDocumentsView.fromPagedState( + DocumentPagingState state, { + super.key, + this.onSelected, + this.onTap, + this.onCorrespondentSelected, + this.onDocumentTypeSelected, + this.onStoragePathSelected, + this.onTagSelected, + this.isLabelClickable = true, + this.enableHeroAnimation = true, + required this.hasInternetConnection, + this.viewType = ViewType.list, + this.selectedDocumentIds = const [], + }) : documents = state.documents, + isLoading = state.isLoading, + hasLoaded = state.hasLoaded; } class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { @@ -115,7 +134,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { (context, index) { final document = documents.elementAt(index); return LabelRepositoriesProvider( - child: DocumentGridItem( + child: DocumentDetailedItem( isLabelClickable: isLabelClickable, document: document, onTap: onTap, @@ -136,7 +155,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { Widget _buildGridView() { if (showLoadingPlaceholder) { - return DocumentGridLoadingWidget.sliver(); + return const DocumentGridLoadingWidget.sliver(); } return SliverGrid.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -205,6 +224,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } return ListView.builder( + padding: EdgeInsets.zero, controller: scrollController, primary: false, itemCount: documents.length, @@ -235,6 +255,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } return ListView.builder( + padding: EdgeInsets.zero, physics: const PageScrollPhysics(), controller: scrollController, primary: false, @@ -265,6 +286,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { return DocumentGridLoadingWidget(); } return GridView.builder( + padding: EdgeInsets.zero, controller: scrollController, primary: false, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( diff --git a/lib/features/documents/view/widgets/document_preview.dart b/lib/features/documents/view/widgets/document_preview.dart index c4107b9..b505ea8 100644 --- a/lib/features/documents/view/widgets/document_preview.dart +++ b/lib/features/documents/view/widgets/document_preview.dart @@ -2,51 +2,59 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; class DocumentPreview extends StatelessWidget { - final int id; + final DocumentModel document; final BoxFit fit; final Alignment alignment; final double borderRadius; final bool enableHero; + final double scale; const DocumentPreview({ super.key, - required this.id, + required this.document, this.fit = BoxFit.cover, - this.alignment = Alignment.center, - this.borderRadius = 8.0, + this.alignment = Alignment.topCenter, + this.borderRadius = 12.0, this.enableHero = true, + this.scale = 1.1, }); @override Widget build(BuildContext context) { - if (!enableHero) { - return _buildPreview(context); - } - return Hero( - tag: "thumb_$id", - child: _buildPreview(context), + return HeroMode( + enabled: enableHero, + child: Hero( + tag: "thumb_${document.id}", + child: _buildPreview(context), + ), ); } - ClipRRect _buildPreview(BuildContext context) { + Widget _buildPreview(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(borderRadius), - child: CachedNetworkImage( - fit: fit, - alignment: Alignment.topCenter, - cacheKey: "thumb_$id", - imageUrl: context.read().getThumbnailUrl(id), - errorWidget: (ctxt, msg, __) => Text(msg), - placeholder: (context, value) => Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: const SizedBox(height: 100, width: 100), + child: Transform.scale( + scale: scale, + child: CachedNetworkImage( + fit: fit, + alignment: alignment, + cacheKey: "thumb_${document.id}", + imageUrl: context + .read() + .getThumbnailUrl(document.id), + errorWidget: (ctxt, msg, __) => Text(msg), + placeholder: (context, value) => Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: const SizedBox(height: 100, width: 100), + ), + cacheManager: context.watch(), ), - cacheManager: context.watch(), ), ); } diff --git a/lib/features/documents/view/widgets/items/document_detailed_item.dart b/lib/features/documents/view/widgets/items/document_detailed_item.dart index e22677b..8b0634a 100644 --- a/lib/features/documents/view/widgets/items/document_detailed_item.dart +++ b/lib/features/documents/view/widgets/items/document_detailed_item.dart @@ -1,6 +1,13 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.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'; class DocumentDetailedItem extends DocumentItem { const DocumentDetailedItem({ @@ -20,12 +27,118 @@ class DocumentDetailedItem extends DocumentItem { @override Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final insets = MediaQuery.of(context).viewInsets; + final padding = MediaQuery.of(context).viewPadding; + final availableHeight = size.height - + insets.top - + insets.bottom - + padding.top - + padding.bottom - + kBottomNavigationBarHeight - + kToolbarHeight; + final maxHeight = min(500.0, availableHeight); return Card( - child: Column( - children: [ - DocumentPreview(id: document.id), - ], + child: InkWell( + enableFeedback: true, + borderRadius: BorderRadius.circular(12), + onTap: () { + if (isSelectionActive) { + onSelected?.call(document); + } else { + onTap?.call(document); + } + }, + onLongPress: () { + onSelected?.call(document); + }, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: double.infinity, + height: maxHeight / 2, + ), + child: DocumentPreview( + document: document, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + DateFormat.yMMMMd().format(document.created), + style: Theme.of(context) + .textTheme + .bodySmall + ?.apply(color: Theme.of(context).hintColor), + ), + if (document.archiveSerialNumber != null) + Row( + children: [ + Text( + '#${document.archiveSerialNumber}', + style: Theme.of(context) + .textTheme + .bodySmall + ?.apply(color: Theme.of(context).hintColor), + ), + ], + ), + ], + ).paddedLTRB(8, 8, 8, 4), + Text( + document.title, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ).paddedLTRB(8, 0, 8, 4), + Row( + children: [ + const Icon( + Icons.person_outline, + size: 16, + ).paddedOnly(right: 4.0), + CorrespondentWidget( + onSelected: onCorrespondentSelected, + textStyle: Theme.of(context).textTheme.titleSmall?.apply( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + correspondentId: document.correspondent, + ), + ], + ).paddedLTRB(8, 0, 8, 4), + Row( + children: [ + const Icon( + Icons.description_outlined, + size: 16, + ).paddedOnly(right: 4.0), + DocumentTypeWidget( + onSelected: onDocumentTypeSelected, + textStyle: Theme.of(context).textTheme.titleSmall?.apply( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + documentTypeId: document.documentType, + ), + ], + ).paddedLTRB(8, 0, 8, 4), + TagsWidget( + isMultiLine: false, + tagIds: document.tags, + ).padded(), + ], + ), + ], + ), ), - ); + ).padded(); } } diff --git a/lib/features/documents/view/widgets/items/document_grid_item.dart b/lib/features/documents/view/widgets/items/document_grid_item.dart index 4c494e8..e8856e0 100644 --- a/lib/features/documents/view/widgets/items/document_grid_item.dart +++ b/lib/features/documents/view/widgets/items/document_grid_item.dart @@ -25,66 +25,62 @@ class DocumentGridItem extends DocumentItem { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: _onTap, - onLongPress: onSelected != null ? () => onSelected!(document) : null, - child: AbsorbPointer( - absorbing: isSelectionActive, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - elevation: 1.0, - color: isSelected - ? Theme.of(context).colorScheme.inversePrimary - : Theme.of(context).cardColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: 1, - child: DocumentPreview( - id: document.id, - borderRadius: 12.0, - enableHero: enableHeroAnimation, + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + elevation: 1.0, + color: isSelected + ? Theme.of(context).colorScheme.inversePrimary + : Theme.of(context).cardColor, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _onTap, + onLongPress: onSelected != null ? () => onSelected!(document) : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 1, + child: DocumentPreview( + document: document, + borderRadius: 12.0, + enableHero: enableHeroAnimation, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CorrespondentWidget( + correspondentId: document.correspondent, + ), + DocumentTypeWidget( + documentTypeId: document.documentType, + ), + Text( + document.title, + maxLines: document.tags.isEmpty ? 3 : 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + TagsWidget( + tagIds: document.tags, + isMultiLine: false, + onTagSelected: onTagSelected, + ), + const Spacer(), + Text( + DateFormat.yMMMd().format(document.created), + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CorrespondentWidget( - correspondentId: document.correspondent, - ), - DocumentTypeWidget( - documentTypeId: document.documentType, - ), - Text( - document.title, - maxLines: document.tags.isEmpty ? 3 : 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - const Spacer(), - TagsWidget( - tagIds: document.tags, - isMultiLine: false, - onTagSelected: onTagSelected, - ), - const Spacer(), - Text( - DateFormat.yMMMd().format( - document.created, - ), - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - ), - ], - ), + ), + ], ), ), ), diff --git a/lib/features/documents/view/widgets/items/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart index fdb2f28..5c1bc8c 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -122,7 +122,7 @@ class DocumentListItem extends DocumentItem { aspectRatio: _a4AspectRatio, child: GestureDetector( child: DocumentPreview( - id: document.id, + document: document, fit: BoxFit.cover, alignment: Alignment.topCenter, enableHero: enableHeroAnimation, diff --git a/lib/features/documents/view/widgets/document_grid_loading_widget.dart b/lib/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart similarity index 69% rename from lib/features/documents/view/widgets/document_grid_loading_widget.dart rename to lib/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart index d18d5d9..5fc7aba 100644 --- a/lib/features/documents/view/widgets/document_grid_loading_widget.dart +++ b/lib/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart @@ -1,24 +1,15 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; -import 'package:shimmer/shimmer.dart'; -class DocumentGridLoadingWidget extends StatelessWidget - with DocumentItemPlaceholder { +class DocumentGridLoadingWidget extends StatelessWidget { final bool _isSliver; @override - final Random random = Random(1257195195); - DocumentGridLoadingWidget({super.key}) : _isSliver = false; + const DocumentGridLoadingWidget({super.key}) : _isSliver = false; - DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true; + const DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true; @override Widget build(BuildContext context) { @@ -41,8 +32,6 @@ class DocumentGridLoadingWidget extends StatelessWidget } Widget _buildPlaceholderGridItem(BuildContext context) { - final values = nextValues; - return Padding( padding: const EdgeInsets.all(8.0), child: Card( @@ -68,21 +57,25 @@ class DocumentGridLoadingWidget extends StatelessWidget child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextPlaceholder( - length: values.correspondentLength, + const TextPlaceholder( + length: 70, + fontSize: 16, + ).padded(1), + const TextPlaceholder( + length: 50, fontSize: 16, ).padded(1), TextPlaceholder( - length: values.titleLength, - fontSize: 16, + length: 200, + fontSize: + Theme.of(context).textTheme.titleMedium?.fontSize ?? + 10, + ).padded(1), + const Spacer(), + const TagsPlaceholder( + count: 2, + dense: true, ), - if (values.tagCount > 0) ...[ - const Spacer(), - TagsPlaceholder( - count: values.tagCount, - dense: true, - ), - ], const Spacer(), TextPlaceholder( length: 100, diff --git a/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart b/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart deleted file mode 100644 index 951e5ff..0000000 --- a/lib/features/documents/view/widgets/placeholder/document_item_placeholder.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:math'; - -mixin DocumentItemPlaceholder { - static const _tags = [" ", " ", " "]; - static const _titleLengths = [double.infinity, 150.0, 200.0]; - static const _correspondentLengths = [120.0, 80.0, 40.0]; - - Random get random; - - RandomDocumentItemPlaceholderValues get nextValues { - return RandomDocumentItemPlaceholderValues( - tagCount: random.nextInt(_tags.length + 1), - correspondentLength: _correspondentLengths[ - random.nextInt(_correspondentLengths.length - 1)], - titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)], - ); - } -} - -class RandomDocumentItemPlaceholderValues { - final int tagCount; - final double correspondentLength; - final double titleLength; - - RandomDocumentItemPlaceholderValues({ - required this.tagCount, - required this.correspondentLength, - required this.titleLength, - }); -} diff --git a/lib/features/documents/view/widgets/documents_list_loading_widget.dart b/lib/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart similarity index 65% rename from lib/features/documents/view/widgets/documents_list_loading_widget.dart rename to lib/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart index 034c074..aed9304 100644 --- a/lib/features/documents/view/widgets/documents_list_loading_widget.dart +++ b/lib/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart @@ -1,21 +1,13 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; -class DocumentsListLoadingWidget extends StatelessWidget - with DocumentItemPlaceholder { +class DocumentsListLoadingWidget extends StatelessWidget { final bool _isSliver; - DocumentsListLoadingWidget({super.key}) : _isSliver = false; + const DocumentsListLoadingWidget({super.key}) : _isSliver = false; - DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true; - - @override - final Random random = Random(1209571050); + const DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true; @override Widget build(BuildContext context) { @@ -35,26 +27,31 @@ class DocumentsListLoadingWidget extends StatelessWidget Widget _buildFakeListItem(BuildContext context) { const fontSize = 14.0; - final values = nextValues; return ShimmerPlaceholder( child: ListTile( contentPadding: const EdgeInsets.all(8), dense: true, isThreeLine: true, leading: ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), child: Container( color: Colors.white, height: double.infinity, width: 35, ), ), - title: Row( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextPlaceholder( - length: values.correspondentLength, + const TextPlaceholder( + length: 120, fontSize: fontSize, ), + const SizedBox(height: 2), + TextPlaceholder( + length: 220, + fontSize: Theme.of(context).textTheme.titleMedium!.fontSize!, + ), ], ), subtitle: Padding( @@ -63,14 +60,10 @@ class DocumentsListLoadingWidget extends StatelessWidget crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + TagsPlaceholder(count: 2, dense: true), + SizedBox(height: 2), TextPlaceholder( - length: values.titleLength, - fontSize: fontSize, - ), - if (values.tagCount > 0) - TagsPlaceholder(count: values.tagCount, dense: true), - TextPlaceholder( - length: 100, + length: 250, fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!, ), ], diff --git a/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart b/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart index 757f3ef..85c5caf 100644 --- a/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart +++ b/lib/features/documents/view/widgets/placeholder/tags_placeholder.dart @@ -15,6 +15,7 @@ class TagsPlaceholder extends StatelessWidget { return SizedBox( height: 32, child: ListView.separated( + padding: EdgeInsets.zero, itemCount: count, scrollDirection: Axis.horizontal, itemBuilder: (context, index) => FilterChip( diff --git a/lib/features/documents/view/widgets/placeholder/text_placeholder.dart b/lib/features/documents/view/widgets/placeholder/text_placeholder.dart index ef02729..caa59b4 100644 --- a/lib/features/documents/view/widgets/placeholder/text_placeholder.dart +++ b/lib/features/documents/view/widgets/placeholder/text_placeholder.dart @@ -1,9 +1,4 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; class TextPlaceholder extends StatelessWidget { final double length; diff --git a/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart b/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart index fdaa64a..f5ff5d6 100644 --- a/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart +++ b/lib/features/documents/view/widgets/selection/view_type_selection_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; /// Meant to be used with blocbuilder. @@ -28,22 +27,26 @@ class ViewTypeSelectionWidget extends StatelessWidget { break; } return PopupMenuButton( - child: Icon(icon), + initialValue: viewType, + icon: Icon(icon), itemBuilder: (context) => [ _buildViewTypeOption( - ViewType.list, - 'List', - Icons.list, + context, + type: ViewType.list, + label: 'List', //TODO: INTL + icon: Icons.list, ), _buildViewTypeOption( - ViewType.grid, - 'Grid', - Icons.grid_view_rounded, + context, + type: ViewType.grid, + label: 'Grid', //TODO: INTL + icon: Icons.grid_view_rounded, ), _buildViewTypeOption( - ViewType.detailed, - 'Detailed', - Icons.article_outlined, + context, + type: ViewType.detailed, + label: 'Detailed', //TODO: INTL + icon: Icons.article_outlined, ), ], onSelected: (next) { @@ -53,17 +56,22 @@ class ViewTypeSelectionWidget extends StatelessWidget { } PopupMenuItem _buildViewTypeOption( - ViewType type, - String label, - IconData icon, - ) { + BuildContext context, { + required ViewType type, + required String label, + required IconData icon, + }) { + final selected = type == viewType; return PopupMenuItem( value: type, child: ListTile( - selected: type == viewType, - trailing: type == viewType ? const Icon(Icons.done) : null, + selected: selected, + trailing: selected ? const Icon(Icons.done) : null, title: Text(label), + iconColor: Theme.of(context).colorScheme.onSurface, + textColor: Theme.of(context).colorScheme.onSurface, leading: Icon(icon), + contentPadding: EdgeInsets.zero, ), ); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 3ee38c7..4c2f048 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -25,7 +25,6 @@ import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; -import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; @@ -63,7 +62,7 @@ class _HomePageState extends State with WidgetsBindingObserver { context.read(), ); _listenToInboxChanges(); - context.read().reload(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _listenForReceivedFiles(); }); @@ -82,12 +81,22 @@ class _HomePageState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed && !_inboxTimer.isActive) { - log('App is now in foreground, start polling for statistics.'); - _listenToInboxChanges(); - } else if (state != AppLifecycleState.resumed) { - log('App is now in background, stop polling for statistics.'); - _inboxTimer.cancel(); + switch (state) { + case AppLifecycleState.resumed: + log('App is now in foreground'); + context.read().reload(); + log("Reloaded device connectivity state"); + if (!_inboxTimer.isActive) { + _listenToInboxChanges(); + } + break; + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + default: + log('App is now in background'); + _inboxTimer.cancel(); + break; } } @@ -272,21 +281,10 @@ class _HomePageState extends State with WidgetsBindingObserver { ], child: const LabelsPage(), ), - MultiBlocProvider( - providers: [ - // We need to manually downcast the inboxcubit to the - // mixed-in DocumentPagingBlocMixin to use the - // DocumentPagingViewMixin in the inbox. - BlocProvider.value( - value: _inboxCubit, - ), - BlocProvider.value( - value: _inboxCubit, - ), - ], + BlocProvider.value( + value: _inboxCubit, child: const InboxPage(), ), - // const SettingsPage(), ]; return MultiBlocListener( listeners: [ diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index fdbaea1..9b0ad58 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -5,10 +5,10 @@ import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart'; @@ -24,7 +24,8 @@ class InboxPage extends StatefulWidget { State createState() => _InboxPageState(); } -class _InboxPageState extends State with DocumentPagingViewMixin { +class _InboxPageState extends State + with DocumentPagingViewMixin { @override final pagingScrollController = ScrollController(); final _emptyStateRefreshIndicatorKey = GlobalKey(); diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 7103169..4953838 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -54,7 +54,7 @@ class _InboxItemState extends State { AspectRatio( aspectRatio: InboxItem._a4AspectRatio, child: DocumentPreview( - id: widget.document.id, + document: widget.document, fit: BoxFit.cover, alignment: Alignment.topCenter, enableHero: false, diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index 1c87774..29e27bf 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -31,6 +31,8 @@ class DocumentTypeWidget extends StatelessWidget { state.labels[documentTypeId]?.toString() ?? "-", style: (textStyle ?? Theme.of(context).textTheme.bodyMedium) ?.copyWith(color: Theme.of(context).colorScheme.tertiary), + overflow: TextOverflow.ellipsis, + maxLines: 1, ); }, ), diff --git a/lib/features/linked_documents/view/linked_documents_page.dart b/lib/features/linked_documents/view/linked_documents_page.dart index 8b77a6b..6cbcec3 100644 --- a/lib/features/linked_documents/view/linked_documents_page.dart +++ b/lib/features/linked_documents/view/linked_documents_page.dart @@ -16,7 +16,7 @@ class LinkedDocumentsPage extends StatefulWidget { } class _LinkedDocumentsPageState extends State - with DocumentPagingViewMixin { + with DocumentPagingViewMixin { @override final pagingScrollController = ScrollController(); diff --git a/lib/features/paged_document_view/view/document_paging_view_mixin.dart b/lib/features/paged_document_view/view/document_paging_view_mixin.dart index 470a4b9..c5f0e3e 100644 --- a/lib/features/paged_document_view/view/document_paging_view_mixin.dart +++ b/lib/features/paged_document_view/view/document_paging_view_mixin.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; -mixin DocumentPagingViewMixin on State { +mixin DocumentPagingViewMixin on State { ScrollController get pagingScrollController; @override @@ -20,7 +20,7 @@ mixin DocumentPagingViewMixin on State { super.dispose(); } - DocumentPagingBlocMixin get _bloc => context.read(); + DocumentPagingBlocMixin get _bloc => context.read(); void shouldLoadMoreDocumentsListener() async { if (shouldLoadMoreDocuments) { diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 23801ff..608bc79 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; @@ -12,50 +13,55 @@ class SavedViewList extends StatelessWidget { @override Widget build(BuildContext context) { final savedViewCubit = context.read(); - return BlocBuilder( - builder: (context, state) { - if (state.value.isEmpty) { - return SliverToBoxAdapter( - child: HintCard( - hintText: S.of(context).savedViewsEmptyStateText, - ), - ); - } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final view = state.value.values.elementAt(index); - return ListTile( - title: Text(view.name), - subtitle: Text( - S - .of(context) - .savedViewsFiltersSetCount(view.filterRules.length), + return BlocBuilder( + builder: (context, connectivity) { + return BlocBuilder( + builder: (context, state) { + if (state.value.isEmpty) { + return SliverToBoxAdapter( + child: HintCard( + hintText: S.of(context).savedViewsEmptyStateText, ), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SavedViewDetailsCubit( - context.read(), - context.read(), - savedView: view, + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final view = state.value.values.elementAt(index); + return ListTile( + enabled: connectivity.isConnected, + title: Text(view.name), + subtitle: Text( + S + .of(context) + .savedViewsFiltersSetCount(view.filterRules.length), + ), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SavedViewDetailsCubit( + context.read(), + context.read(), + savedView: view, + ), + ), + ], + child: SavedViewDetailsPage( + onDelete: savedViewCubit.remove, ), ), - ], - child: SavedViewDetailsPage( - onDelete: savedViewCubit.remove, ), - ), - ), + ); + }, ); }, - ); - }, - childCount: state.value.length, - ), + childCount: state.value.length, + ), + ); + }, ); }, ); diff --git a/lib/features/saved_view_details/view/saved_view_details_page.dart b/lib/features/saved_view_details/view/saved_view_details_page.dart index d6a356c..bd40803 100644 --- a/lib/features/saved_view_details/view/saved_view_details_page.dart +++ b/lib/features/saved_view_details/view/saved_view_details_page.dart @@ -22,7 +22,7 @@ class SavedViewDetailsPage extends StatefulWidget { } class _SavedViewDetailsPageState extends State - with DocumentPagingViewMixin { + with DocumentPagingViewMixin { @override final pagingScrollController = ScrollController(); @@ -56,7 +56,7 @@ class _SavedViewDetailsPageState extends State onChanged: cubit.setViewType, ); }, - ) + ), ], ), body: BlocBuilder( diff --git a/lib/features/search_app_bar/view/search_app_bar.dart b/lib/features/search_app_bar/view/search_app_bar.dart index ee70481..1b1eb8b 100644 --- a/lib/features/search_app_bar/view/search_app_bar.dart +++ b/lib/features/search_app_bar/view/search_app_bar.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; @@ -29,13 +32,13 @@ class _SearchAppBarState extends State { @override Widget build(BuildContext context) { return SliverAppBar( - automaticallyImplyLeading: false, floating: true, pinned: true, snap: true, + automaticallyImplyLeading: false, backgroundColor: widget.backgroundColor, title: SearchBar( - height: kToolbarHeight - 8, + height: kToolbarHeight - 12, supportingText: widget.hintText, onTap: () => widget.onOpenSearch(context), leadingIcon: IconButton( @@ -43,17 +46,22 @@ class _SearchAppBarState extends State { onPressed: Scaffold.of(context).openDrawer, ), trailingIcon: IconButton( - icon: const CircleAvatar( - child: Text("A"), + icon: BlocBuilder( + builder: (context, state) { + return CircleAvatar( + child: Text(state.information?.userInitials ?? ''), + ); + }, ), onPressed: () { showDialog( context: context, - builder: (context) => AccountSettingsDialog(), + builder: (context) => const AccountSettingsDialog(), ); }, ), - ).paddedOnly(top: 4, bottom: 4), + ).paddedOnly(top: 8, bottom: 4), bottom: widget.bottom, ); } diff --git a/lib/features/settings/view/dialogs/account_settings_dialog.dart b/lib/features/settings/view/dialogs/account_settings_dialog.dart index b2b5c39..287be24 100644 --- a/lib/features/settings/view/dialogs/account_settings_dialog.dart +++ b/lib/features/settings/view/dialogs/account_settings_dialog.dart @@ -35,10 +35,7 @@ class AccountSettingsDialog extends StatelessWidget { children: [ ExpansionTile( leading: CircleAvatar( - child: Text(state.information?.username - ?.toUpperCase() - .substring(0, 1) ?? - ''), + child: Text(state.information?.userInitials ?? ''), ), title: Text(state.information?.username ?? ''), subtitle: Text(state.information?.host ?? ''), diff --git a/lib/features/similar_documents/view/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart index 826801e..b386a0f 100644 --- a/lib/features/similar_documents/view/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/widgets/offline_widget.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/paged_document_view/view/document_paging_view_mixin.dart'; @@ -17,7 +18,7 @@ class SimilarDocumentsView extends StatefulWidget { } class _SimilarDocumentsViewState extends State - with DocumentPagingViewMixin { + with DocumentPagingViewMixin { @override final pagingScrollController = ScrollController(); @@ -33,44 +34,50 @@ class _SimilarDocumentsViewState extends State @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { - return DocumentsEmptyState( - state: state, - onReset: () => context.read().updateFilter( - filter: DocumentFilter.initial.copyWith( - moreLike: () => - context.read().documentId, - ), - ), - ); - } - - return BlocBuilder( - builder: (context, connectivity) { - return CustomScrollView( - controller: pagingScrollController, - slivers: [ - SliverAdaptiveDocumentsView( - documents: state.documents, - hasInternetConnection: connectivity.isConnected, - isLabelClickable: false, - isLoading: state.isLoading, - hasLoaded: state.hasLoaded, - enableHeroAnimation: false, - onTap: (document) { - Navigator.pushNamed( - context, - DocumentDetailsRoute.routeName, - arguments: DocumentDetailsRouteArguments( - document: document, - isLabelClickable: false, + return BlocConsumer( + listenWhen: (previous, current) => + !previous.isConnected && current.isConnected, + listener: (context, state) => + context.read().initialize(), + builder: (context, connectivity) { + return BlocBuilder( + builder: (context, state) { + if (!connectivity.isConnected && !state.hasLoaded) { + return const OfflineWidget(); + } + if (state.hasLoaded && + !state.isLoading && + state.documents.isEmpty) { + return DocumentsEmptyState( + state: state, + onReset: () => context + .read() + .updateFilter( + filter: DocumentFilter.initial.copyWith( + moreLike: () => + context.read().documentId, ), - ); - }, - ), - ], + ), + ); + } + return DefaultAdaptiveDocumentsView( + scrollController: pagingScrollController, + documents: state.documents, + hasInternetConnection: connectivity.isConnected, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + enableHeroAnimation: false, + onTap: (document) { + Navigator.pushNamed( + context, + DocumentDetailsRoute.routeName, + arguments: DocumentDetailsRouteArguments( + document: document, + isLabelClickable: false, + ), + ); + }, ); }, ); diff --git a/packages/paperless_api/lib/src/models/paperless_server_information_model.dart b/packages/paperless_api/lib/src/models/paperless_server_information_model.dart index a3c9db4..16eb623 100644 --- a/packages/paperless_api/lib/src/models/paperless_server_information_model.dart +++ b/packages/paperless_api/lib/src/models/paperless_server_information_model.dart @@ -9,6 +9,10 @@ class PaperlessServerInformationModel { final String? username; final String? host; + String? get userInitials { + return username?.substring(0, 1).toUpperCase(); + } + PaperlessServerInformationModel({ this.host, this.username,