diff --git a/README.md b/README.md index 9df5a4b..6dfe824 100644 --- a/README.md +++ b/README.md @@ -75,22 +75,17 @@ To get a local copy up and running follow these simple steps. ### Prerequisites * Install an IDE of your choice (e.g. VSCode with the Dart/Flutter extensions) - +* Install the flutter SDK (https://docs.flutter.dev/get-started/install) _or_ use the flutter git submodule pinned in this project by running `git submodule update --init` inside the project root directory. +* ### Install dependencies and generate files 1. First, clone the repository: ```sh git clone https://github.com/astubenbord/paperless-mobile.git ``` -In this project, flutter is pinned at a specific version as a git submodule to ensure all contributors work with the same environment and build with the same flutter version. You can also use your local flutter installation, just make sure that the app also compiles with the same flutter version as pinned in the `flutter` submodule when opening a pull request. -To download the pinned flutter SDK from the submodule and plan to install the dependencies manually in the next step, simply run -```sh -git submodule update --init -``` +You can now run the `scripts/install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate files for both the app and local packages. -You can now run the `scripts/install_dependencies.sh` script at the root of the project, which will automatically install dependencies and generate files for both the app and subpackages. Note that the `install_dependencies.sh` script will pull the flutter submodule and use the SDK to execute the flutter commands. - -If you don't want to use submodules, you can also run the following commands using your local flutter installation: +If you want to manually install dependencies and build generated files, you can also run the following commands: #### Inside the `packages/paperless_api/` folder: 2. Install the dependencies for `paperless_api` diff --git a/lib/core/interceptor/dio_http_error_interceptor.dart b/lib/core/interceptor/dio_http_error_interceptor.dart index 7e67790..b5a0700 100644 --- a/lib/core/interceptor/dio_http_error_interceptor.dart +++ b/lib/core/interceptor/dio_http_error_interceptor.dart @@ -22,7 +22,7 @@ class DioHttpErrorInterceptor extends Interceptor { DioError( error: const PaperlessServerException(ErrorCode.deviceOffline), requestOptions: err.requestOptions, - type: DioErrorType.connectTimeout, + type: DioErrorType.connectionTimeout, ), ); } @@ -52,7 +52,7 @@ class DioHttpErrorInterceptor extends Interceptor { DioError( error: errorMessages, requestOptions: err.requestOptions, - type: DioErrorType.response, + type: DioErrorType.badResponse, ), ); } @@ -66,7 +66,7 @@ class DioHttpErrorInterceptor extends Interceptor { handler.reject( DioError( requestOptions: err.requestOptions, - type: DioErrorType.response, + type: DioErrorType.badResponse, error: const PaperlessServerException( ErrorCode.missingClientCertificate), ), diff --git a/lib/core/interceptor/retry_on_connection_change_interceptor.dart b/lib/core/interceptor/retry_on_connection_change_interceptor.dart index e6f90a6..6c78173 100644 --- a/lib/core/interceptor/retry_on_connection_change_interceptor.dart +++ b/lib/core/interceptor/retry_on_connection_change_interceptor.dart @@ -28,10 +28,12 @@ class RetryOnConnectionChangeInterceptor extends Interceptor { } bool _shouldRetryOnHttpException(DioError err) { - return err.type == DioErrorType.other && - ((err.error is HttpException && - err.message.contains( - 'Connection closed before full header was received'))); + return err.type == DioErrorType.unknown && + (err.error is HttpException && + (err.message?.contains( + 'Connection closed before full header was received', + ) ?? + false)); } } diff --git a/lib/core/interceptor/server_reachability_error_interceptor.dart b/lib/core/interceptor/server_reachability_error_interceptor.dart index bfc4183..a895451 100644 --- a/lib/core/interceptor/server_reachability_error_interceptor.dart +++ b/lib/core/interceptor/server_reachability_error_interceptor.dart @@ -19,7 +19,7 @@ class ServerReachabilityErrorInterceptor extends Interceptor { ); } } - if (err.type == DioErrorType.connectTimeout) { + if (err.type == DioErrorType.connectionTimeout) { return _rejectWithStatus( ReachabilityStatus.connectionTimeout, err, @@ -55,6 +55,6 @@ void _rejectWithStatus( error: reachabilityStatus, requestOptions: err.requestOptions, response: err.response, - type: DioErrorType.other, + type: DioErrorType.unknown, )); } diff --git a/lib/core/security/session_manager.dart b/lib/core/security/session_manager.dart index bb57078..37c0c58 100644 --- a/lib/core/security/session_manager.dart +++ b/lib/core/security/session_manager.dart @@ -1,9 +1,9 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; @@ -19,10 +19,12 @@ class SessionManager { static Dio _initDio(List interceptors) { //en- and decoded by utf8 by default - final Dio dio = Dio(BaseOptions()); - dio.options.receiveTimeout = const Duration(seconds: 25).inMilliseconds; + final Dio dio = Dio( + BaseOptions(contentType: Headers.jsonContentType), + ); + dio.options.receiveTimeout = const Duration(seconds: 25); dio.options.responseType = ResponseType.json; - (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = (client) => client..badCertificateCallback = (cert, host, port) => true; dio.interceptors.addAll([ ...interceptors, @@ -59,7 +61,7 @@ class SessionManager { clientCertificate.bytes, password: clientCertificate.passphrase, ); - final adapter = DefaultHttpClientAdapter() + final adapter = IOHttpClientAdapter() ..onHttpClientCreate = (client) => HttpClient(context: context) ..badCertificateCallback = (X509Certificate cert, String host, int port) => true; @@ -72,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) { @@ -81,9 +85,9 @@ class SessionManager { } void resetSettings() { - client.httpClientAdapter = DefaultHttpClientAdapter(); + client.httpClientAdapter = IOHttpClientAdapter(); client.options.baseUrl = ''; - client.options.headers.remove('Authorization'); + client.options.headers.remove(HttpHeaders.authorizationHeader); serverInformation = PaperlessServerInformationModel(); } } diff --git a/lib/core/service/connectivity_status_service.dart b/lib/core/service/connectivity_status_service.dart index dca7eae..58967cb 100644 --- a/lib/core/service/connectivity_status_service.dart +++ b/lib/core/service/connectivity_status_service.dart @@ -1,8 +1,6 @@ -import 'dart:developer'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; import 'package:paperless_mobile/core/global/os_error_codes.dart'; import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart'; @@ -71,8 +69,8 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService { SessionManager manager = SessionManager([ServerReachabilityErrorInterceptor()]) ..updateSettings(clientCertificate: clientCertificate) - ..client.options.connectTimeout = 5000 - ..client.options.receiveTimeout = 5000; + ..client.options.connectTimeout = const Duration(seconds: 5) + ..client.options.receiveTimeout = const Duration(seconds: 5); final response = await manager.client.get('$serverAddress/api/'); if (response.statusCode == 200) { @@ -80,7 +78,7 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService { } return ReachabilityStatus.notReachable; } on DioError catch (error) { - if (error.type == DioErrorType.other && + if (error.type == DioErrorType.unknown && error.error is ReachabilityStatus) { return error.error as ReachabilityStatus; } 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 fa68612..350b9fc 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 eb997a5..e8c200a 100644 --- a/lib/features/documents/view/widgets/adaptive_documents_view.dart +++ b/lib/features/documents/view/widgets/adaptive_documents_view.dart @@ -1,10 +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 { @@ -41,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 { @@ -69,6 +89,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { return _buildGridView(); case ViewType.list: return _buildListView(); + case ViewType.detailed: + return _buildFullView(context); } } @@ -101,9 +123,39 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView { ); } + Widget _buildFullView(BuildContext context) { + if (showLoadingPlaceholder) { + //TODO: Build detailed loading animation + return DocumentsListLoadingWidget.sliver(); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + childCount: documents.length, + (context, index) { + final document = documents.elementAt(index); + return LabelRepositoriesProvider( + child: DocumentDetailedItem( + isLabelClickable: isLabelClickable, + document: document, + onTap: onTap, + isSelected: selectedDocumentIds.contains(document.id), + onSelected: onSelected, + isSelectionActive: selectedDocumentIds.isNotEmpty, + onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, + ), + ); + }, + ), + ); + } + Widget _buildGridView() { if (showLoadingPlaceholder) { - return DocumentGridLoadingWidget.sliver(); + return const DocumentGridLoadingWidget.sliver(); } return SliverGrid.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -161,6 +213,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { return _buildGridView(); case ViewType.list: return _buildListView(); + case ViewType.detailed: + return _buildFullView(); } } @@ -170,6 +224,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { } return ListView.builder( + padding: EdgeInsets.zero, controller: scrollController, primary: false, itemCount: documents.length, @@ -194,11 +249,44 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView { ); } + Widget _buildFullView() { + if (showLoadingPlaceholder) { + return DocumentsListLoadingWidget(); + } + + return ListView.builder( + padding: EdgeInsets.zero, + physics: const PageScrollPhysics(), + controller: scrollController, + primary: false, + itemCount: documents.length, + itemBuilder: (context, index) { + final document = documents.elementAt(index); + return LabelRepositoriesProvider( + child: DocumentDetailedItem( + isLabelClickable: isLabelClickable, + document: document, + onTap: onTap, + isSelected: selectedDocumentIds.contains(document.id), + onSelected: onSelected, + isSelectionActive: selectedDocumentIds.isNotEmpty, + onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, + enableHeroAnimation: enableHeroAnimation, + ), + ); + }, + ); + } + Widget _buildGridView() { if (showLoadingPlaceholder) { 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 new file mode 100644 index 0000000..8b0634a --- /dev/null +++ b/lib/features/documents/view/widgets/items/document_detailed_item.dart @@ -0,0 +1,144 @@ +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({ + super.key, + required super.document, + required super.isSelected, + required super.isSelectionActive, + required super.isLabelClickable, + required super.enableHeroAnimation, + super.onCorrespondentSelected, + super.onDocumentTypeSelected, + super.onSelected, + super.onStoragePathSelected, + super.onTagSelected, + super.onTap, + }); + + @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: 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 66ae74c..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 @@ -14,13 +14,65 @@ class ViewTypeSelectionWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final next = viewType.toggle(); - final icon = next == ViewType.grid ? Icons.grid_view_rounded : Icons.list; - return IconButton( + late final IconData icon; + switch (viewType) { + case ViewType.grid: + icon = Icons.grid_view_rounded; + break; + case ViewType.list: + icon = Icons.list; + break; + case ViewType.detailed: + icon = Icons.article_outlined; + break; + } + return PopupMenuButton( + initialValue: viewType, icon: Icon(icon), - onPressed: () { + itemBuilder: (context) => [ + _buildViewTypeOption( + context, + type: ViewType.list, + label: 'List', //TODO: INTL + icon: Icons.list, + ), + _buildViewTypeOption( + context, + type: ViewType.grid, + label: 'Grid', //TODO: INTL + icon: Icons.grid_view_rounded, + ), + _buildViewTypeOption( + context, + type: ViewType.detailed, + label: 'Detailed', //TODO: INTL + icon: Icons.article_outlined, + ), + ], + onSelected: (next) { onChanged(next); }, ); } + + PopupMenuItem _buildViewTypeOption( + BuildContext context, { + required ViewType type, + required String label, + required IconData icon, + }) { + final selected = type == viewType; + return PopupMenuItem( + value: type, + child: ListTile( + 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/model/view_type.dart b/lib/features/settings/model/view_type.dart index 6407dec..57ff3c5 100644 --- a/lib/features/settings/model/view_type.dart +++ b/lib/features/settings/model/view_type.dart @@ -1,8 +1,9 @@ enum ViewType { grid, - list; + list, + detailed; ViewType toggle() { - return this == grid ? list : grid; + return ViewType.values[(index + 1) % ViewType.values.length]; } } 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, diff --git a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart index 171f67d..d2b313d 100644 --- a/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/authentication_api/authentication_api_impl.dart @@ -24,7 +24,7 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi { } on DioError catch (error) { if (error.error is PaperlessServerException || error.error is Map) { - throw error.error; + throw error.error as Map; } else { throw PaperlessServerException( ErrorCode.authenticationFailed, 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 c5f9fea..eafbf71 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 @@ -2,14 +2,10 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/src/constants.dart'; -import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { - static const _dateTimeConverter = LocalDateTimeJsonConverter(); - final Dio client; PaperlessDocumentsApiImpl(this.client); @@ -65,7 +61,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { ); } } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -82,7 +78,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { throw const PaperlessServerException(ErrorCode.documentUpdateFailed); } } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -109,7 +105,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { throw const PaperlessServerException(ErrorCode.documentLoadFailed); } } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -123,7 +119,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } throw const PaperlessServerException(ErrorCode.documentDeleteFailed); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -150,7 +146,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } throw const PaperlessServerException(ErrorCode.documentPreviewFailed); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -172,7 +168,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } on PaperlessServerException { throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -191,7 +187,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { ); } } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -208,7 +204,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { ); return response.data; } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -222,7 +218,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { response.data as Map, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -241,7 +237,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } throw const PaperlessServerException(ErrorCode.autocompleteQueryError); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -256,7 +252,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { } throw const PaperlessServerException(ErrorCode.suggestionsQueryError); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -270,7 +266,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { return null; } } on DioError catch (err) { - throw err.error; + throw err.error!; } } } diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart index 1bff1ca..5af3579 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart @@ -103,7 +103,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -122,7 +122,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -142,7 +142,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -160,7 +160,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -178,7 +178,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -195,7 +195,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -215,7 +215,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -235,7 +235,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -256,7 +256,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -273,7 +273,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -316,7 +316,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -333,7 +333,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { } throw const PaperlessServerException(ErrorCode.unknown); } on DioError catch (err) { - throw err.error; + throw err.error!; } } } diff --git a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart index 21a5693..609f750 100644 --- a/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/saved_views_api/paperless_saved_views_api_impl.dart @@ -39,7 +39,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -55,7 +55,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi { httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } diff --git a/packages/paperless_api/lib/src/request_utils.dart b/packages/paperless_api/lib/src/request_utils.dart index 12add18..0972fcc 100644 --- a/packages/paperless_api/lib/src/request_utils.dart +++ b/packages/paperless_api/lib/src/request_utils.dart @@ -29,7 +29,7 @@ Future getSingleResult( httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } @@ -66,12 +66,13 @@ Future> getCollection( httpStatusCode: response.statusCode, ); } on DioError catch (err) { - throw err.error; + throw err.error!; } } List _collectionFromJson( - _CollectionFromJsonSerializationParams params) { + _CollectionFromJsonSerializationParams params, +) { return params.list.map((result) => params.fromJson(result)).toList(); } diff --git a/packages/paperless_api/pubspec.yaml b/packages/paperless_api/pubspec.yaml index 396538a..9685b52 100644 --- a/packages/paperless_api/pubspec.yaml +++ b/packages/paperless_api/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: http: ^0.13.5 json_annotation: ^4.7.0 intl: ^0.17.0 - dio: ^4.0.6 + dio: ^5.0.0 collection: ^1.17.0 jiffy: ^5.0.0 diff --git a/pubspec.lock b/pubspec.lock index f7cdcaa..0f03455 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: args - sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" asn1lib: dependency: transitive description: @@ -101,18 +101,18 @@ packages: dependency: transitive description: name: bloc - sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a + sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.1" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "622b97678bf8c06a94f4c26a89ee9ebf7319bf775383dee2233e86e1f94ee28d" + sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" boolean_selector: dependency: transitive description: @@ -253,50 +253,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "3f8fe4e504c2d33696dac671a54909743bc6a902a9bb0902306f7a2aed7e528e" + sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96" url: "https://pub.dev" source: hosted - version: "2.3.9" - connectivity_plus_linux: - dependency: transitive - description: - name: connectivity_plus_linux - sha256: "3caf859d001f10407b8e48134c761483e4495ae38094ffcca97193f6c271f5e2" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - connectivity_plus_macos: - dependency: transitive - description: - name: connectivity_plus_macos - sha256: "488d2de1e47e1224ad486e501b20b088686ba1f4ee9c4420ecbc3b9824f0b920" - url: "https://pub.dev" - source: hosted - version: "1.2.6" + version: "3.0.3" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: b8795b9238bf83b64375f63492034cb3d8e222af4d9ce59dda085edf038fa06f + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a url: "https://pub.dev" source: hosted - version: "1.2.3" - connectivity_plus_web: - dependency: transitive - description: - name: connectivity_plus_web - sha256: "81332be1b4baf8898fed17bb4fdef27abb7c6fd990bf98c54fd978478adf2f1a" - url: "https://pub.dev" - source: hosted - version: "1.2.5" - connectivity_plus_windows: - dependency: transitive - description: - name: connectivity_plus_windows - sha256: "535b0404b4d5605c4dd8453d67e5d6d2ea0dd36e3b477f50f31af51b0aeab9dd" - url: "https://pub.dev" - source: hosted - version: "1.2.2" + version: "1.2.4" convert: dependency: transitive description: @@ -309,10 +277,10 @@ packages: dependency: transitive description: name: coverage - sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040" + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" url: "https://pub.dev" source: hosted - version: "1.6.2" + version: "1.6.3" cross_file: dependency: transitive description: @@ -341,18 +309,18 @@ packages: dependency: "direct dev" description: name: dart_code_metrics - sha256: bb4ec5e729788dde5f7e8e9df4c05ec3b78532a5763e635337153ce40085514b + sha256: "026e28da197a03caeccccc0b174ec98ef03da3c81c4543314d7add121aab4375" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.6.0" dart_code_metrics_presets: dependency: transitive description: name: dart_code_metrics_presets - sha256: "43dc1fdcb424fc3aa79964304d09eeda4f199351c52cdc854f8228a9d0296b60" + sha256: "9c51724f836aebc4465228954cb5757e5a99737af26a452b5dec0a2d5d0b4d66" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" dart_style: dependency: transitive description: @@ -437,10 +405,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" dots_indicator: dependency: transitive description: @@ -547,10 +515,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557" + sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.1.2" flutter_blurhash: dependency: transitive description: @@ -735,18 +703,18 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "7cc92eabe01e3f1babe1571c5560b135dfc762a34e41e9056881e2196b178ec1" + sha256: "774fa28b07f3a82c93596bc137be33189fec578ed3447a93a5a11c93435de394" url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" font_awesome_flutter: dependency: "direct main" description: name: font_awesome_flutter - sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc" + sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206" url: "https://pub.dev" source: hosted - version: "10.3.0" + version: "10.4.0" form_builder_validators: dependency: "direct main" description: @@ -836,10 +804,10 @@ packages: dependency: "direct main" description: name: hydrated_bloc - sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7" + sha256: eb92d88061b6b911c48779b08a91c8a9f3a3aa8475f80d9380045375d9876536 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.1.0" image: dependency: "direct main" description: @@ -937,10 +905,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: ba48fe0e1cae140a0813ce68c2540250d7f573a8ae4d4b6c681b2d2583584953 + sha256: cfcbc4936e288d61ef85a04feef6b95f49ba496d4fd98364e6abafb462b06a1f url: "https://pub.dev" source: hosted - version: "1.0.17" + version: "1.0.18" local_auth_ios: dependency: transitive description: @@ -1176,10 +1144,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" path_provider_platform_interface: dependency: transitive description: @@ -1312,10 +1280,10 @@ packages: dependency: "direct main" description: name: pretty_dio_logger - sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" + sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2" url: "https://pub.dev" source: hosted - version: "1.2.0-beta-1" + version: "1.3.1" process: dependency: transitive description: @@ -1392,10 +1360,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e + sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" share_plus_platform_interface: dependency: transitive description: @@ -1685,10 +1653,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" + sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b url: "https://pub.dev" source: hosted - version: "6.1.8" + version: "6.1.9" url_launcher_android: dependency: transitive description: @@ -1701,10 +1669,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 + sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" url: "https://pub.dev" source: hosted - version: "6.0.18" + version: "6.1.0" url_launcher_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4f284a5..035c1c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: package_info_plus: ^1.4.3+1 font_awesome_flutter: ^10.1.0 local_auth: ^2.1.2 - connectivity_plus: ^2.3.9 + connectivity_plus: ^3.0.3 flutter_native_splash: ^2.2.11 share_plus: ^6.2.0 @@ -77,7 +77,7 @@ dependencies: badges: ^2.0.3 flutter_colorpicker: ^1.0.3 provider: ^6.0.5 - dio: ^4.0.6 + dio: ^5.0.0 hydrated_bloc: ^9.0.0 json_annotation: ^4.7.0 pretty_dio_logger: ^1.2.0-beta-1 diff --git a/install_dependencies.sh b/scripts/install_dependencies.sh old mode 100755 new mode 100644 similarity index 81% rename from install_dependencies.sh rename to scripts/install_dependencies.sh index ed3818f..5af7cd8 --- a/install_dependencies.sh +++ b/scripts/install_dependencies.sh @@ -1,8 +1,9 @@ #!/bin/bash +pushd ../ pushd packages/paperless_api flutter pub get flutter pub run build_runner build --delete-conflicting-outputs popd flutter pub get flutter pub run build_runner build --delete-conflicting-outputs -flutter pub run intl_utils:generate \ No newline at end of file +flutter pub run intl_utils:generate