From d2b428c05ba55f12da42902ef626f4c93715d533 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Fri, 2 Jun 2023 16:37:03 +0200 Subject: [PATCH 1/4] feat: Add suggestions to server login page --- lib/core/config/hive/hive_config.dart | 1 + .../form_builder_type_ahead.dart | 13 +- .../login/cubit/authentication_cubit.dart | 6 +- lib/features/login/view/login_page.dart | 23 ++- .../server_address_form_field.dart | 144 ++++++++++++++---- .../login_pages/server_connection_page.dart | 2 +- lib/main.dart | 1 + 7 files changed, 146 insertions(+), 44 deletions(-) diff --git a/lib/core/config/hive/hive_config.dart b/lib/core/config/hive/hive_config.dart index dc231af..c0d8f7b 100644 --- a/lib/core/config/hive/hive_config.dart +++ b/lib/core/config/hive/hive_config.dart @@ -20,6 +20,7 @@ class HiveBoxes { static const localUserAccount = 'localUserAccount'; static const localUserAppState = 'localUserAppState'; static const localUserSettings = 'localUserSettings'; + static const hosts = 'hosts'; } class HiveTypeIds { diff --git a/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart b/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart index bbd8480..18b9f50 100644 --- a/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart +++ b/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart @@ -334,8 +334,7 @@ class FormBuilderTypeAhead extends FormBuilderField { // TODO HACK to satisfy strictness suggestionsCallback: suggestionsCallback, itemBuilder: itemBuilder, - transitionBuilder: (context, suggestionsBox, controller) => - suggestionsBox, + transitionBuilder: (context, suggestionsBox, controller) => suggestionsBox, onSuggestionSelected: (T suggestion) { state.didChange(suggestion); onSuggestionSelected?.call(suggestion); @@ -357,8 +356,7 @@ class FormBuilderTypeAhead extends FormBuilderField { keepSuggestionsOnLoading: keepSuggestionsOnLoading, autoFlipDirection: autoFlipDirection, suggestionsBoxController: suggestionsBoxController, - keepSuggestionsOnSuggestionSelected: - keepSuggestionsOnSuggestionSelected, + keepSuggestionsOnSuggestionSelected: keepSuggestionsOnSuggestionSelected, hideKeyboard: hideKeyboard, scrollController: scrollController, ); @@ -369,15 +367,14 @@ class FormBuilderTypeAhead extends FormBuilderField { FormBuilderTypeAheadState createState() => FormBuilderTypeAheadState(); } -class FormBuilderTypeAheadState - extends FormBuilderFieldState, T> { +class FormBuilderTypeAheadState extends FormBuilderFieldState, T> { late TextEditingController _typeAheadController; @override void initState() { super.initState(); - _typeAheadController = widget.controller ?? - TextEditingController(text: _getTextString(initialValue)); + _typeAheadController = + widget.controller ?? TextEditingController(text: _getTextString(initialValue)); // _typeAheadController.addListener(_handleControllerChanged); } diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 2afdd1a..1cdb368 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -244,7 +244,7 @@ class AuthenticationCubit extends Cubit { final userStateBox = Hive.box(HiveBoxes.localUserAppState); if (userAccountBox.containsKey(localUserId)) { - throw Exception("User with id $localUserId already exists!"); + throw Exception("User already exists!"); } final apiVersion = await _getApiVersion(sessionManager.client); @@ -282,6 +282,10 @@ class AuthenticationCubit extends Cubit { ), ); }); + final hostsBox = Hive.box(HiveBoxes.hosts); + if (!hostsBox.values.contains(serverUrl)) { + await hostsBox.add(serverUrl); + } return serverUser.id; } diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 3e63b36..229bdb1 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; @@ -14,12 +16,13 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'widgets/login_pages/server_login_page.dart'; import 'widgets/never_scrollable_scroll_behavior.dart'; class LoginPage extends StatefulWidget { - final void Function( + final FutureOr Function( BuildContext context, String username, String password, @@ -131,13 +134,17 @@ class _LoginPageState extends State { ); } final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; - widget.onSubmit( - context, - credentials.username!, - credentials.password!, - form[ServerAddressFormField.fkServerAddress], - clientCert, - ); + try { + await widget.onSubmit( + context, + credentials.username!, + credentials.password!, + form[ServerAddressFormField.fkServerAddress], + clientCert, + ); + } on Exception catch (error) { + showGenericError(context, error); + } } } } diff --git a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart index 9d0b5a0..6b0d3eb 100644 --- a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart @@ -1,16 +1,18 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; - final void Function(String? address) onDone; + final void Function(String? address) onSubmit; const ServerAddressFormField({ Key? key, - required this.onDone, + required this.onSubmit, }) : super(key: key); @override @@ -24,21 +26,18 @@ class _ServerAddressFormFieldState extends State { void initState() { super.initState(); _textEditingController.addListener(() { - if (_textEditingController.text.isNotEmpty) { - setState(() { - _canClear = true; - }); - } + setState(() { + _canClear = _textEditingController.text.isNotEmpty; + }); }); } + final _focusNode = FocusNode(); final _textEditingController = TextEditingController(); @override Widget build(BuildContext context) { - return FormBuilderTextField( - key: const ValueKey('login-server-address'), - controller: _textEditingController, + return FormBuilderField( name: ServerAddressFormField.fkServerAddress, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { @@ -50,20 +49,60 @@ class _ServerAddressFormFieldState extends State { } return null; }, - decoration: InputDecoration( - hintText: "http://192.168.1.50:8000", - labelText: S.of(context)!.serverAddress, - suffixIcon: _canClear - ? IconButton( - icon: const Icon(Icons.clear), - color: Theme.of(context).iconTheme.color, - onPressed: () { - _textEditingController.clear(); - }, - ) - : null, - ), - onSubmitted: (_) => _formatInput(), + builder: (field) { + return RawAutocomplete( + focusNode: _focusNode, + textEditingController: _textEditingController, + optionsViewBuilder: (context, onSelected, options) { + return _AutocompleteOptions( + onSelected: onSelected, + options: options, + maxOptionsHeight: 200.0, + ); + }, + key: const ValueKey('login-server-address'), + optionsBuilder: (textEditingValue) { + return Hive.box(HiveBoxes.hosts) + .values + .where((element) => element.contains(textEditingValue.text)); + }, + onSelected: (option) => _formatInput(), + fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + decoration: InputDecoration( + hintText: "http://192.168.1.50:8000", + labelText: S.of(context)!.serverAddress, + suffixIcon: _canClear + ? IconButton( + icon: const Icon(Icons.clear), + color: Theme.of(context).iconTheme.color, + onPressed: () { + textEditingController.clear(); + field.didChange(textEditingController.text); + widget.onSubmit(textEditingController.text); + }, + ) + : null, + ), + autofocus: true, + onSubmitted: (_) { + onFieldSubmitted(); + _formatInput(); + }, + keyboardType: TextInputType.url, + onChanged: (value) { + field.didChange(value); + }, + onEditingComplete: () { + field.didChange(_textEditingController.text); + _focusNode.unfocus(); + }, + ); + }, + ); + }, ); } @@ -71,6 +110,59 @@ class _ServerAddressFormFieldState extends State { String address = _textEditingController.text.trim(); address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); _textEditingController.text = address; - widget.onDone(address); + widget.onSubmit(address); + } +} + +/// Taken from [Autocomplete] +class _AutocompleteOptions extends StatelessWidget { + const _AutocompleteOptions({ + required this.onSelected, + required this.options, + required this.maxOptionsHeight, + }); + + final AutocompleteOnSelected onSelected; + + final Iterable options; + final double maxOptionsHeight; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxOptionsHeight), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final option = options.elementAt(index); + return InkWell( + onTap: () { + onSelected(option); + }, + child: Builder(builder: (BuildContext context) { + final bool highlight = AutocompleteHighlightedOption.of(context) == index; + if (highlight) { + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { + Scrollable.ensureVisible(context, alignment: 0.5); + }); + } + return Container( + color: highlight ? Theme.of(context).focusColor : null, + padding: const EdgeInsets.all(16.0), + child: Text(option), + ); + }), + ); + }, + ), + ), + ), + ); } } diff --git a/lib/features/login/view/widgets/login_pages/server_connection_page.dart b/lib/features/login/view/widgets/login_pages/server_connection_page.dart index eaf6b66..3662f65 100644 --- a/lib/features/login/view/widgets/login_pages/server_connection_page.dart +++ b/lib/features/login/view/widgets/login_pages/server_connection_page.dart @@ -48,7 +48,7 @@ class _ServerConnectionPageState extends State { child: Column( children: [ ServerAddressFormField( - onDone: (address) { + onSubmit: (address) { _updateReachability(address); }, ).padded(), diff --git a/lib/main.dart b/lib/main.dart index e6399eb..5dc43f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -64,6 +64,7 @@ Future _initHive() async { // await getApplicationDocumentsDirectory().then((value) => value.deleteSync(recursive: true)); await Hive.openBox(HiveBoxes.localUserAccount); await Hive.openBox(HiveBoxes.localUserAppState); + await Hive.openBox(HiveBoxes.hosts); final globalSettingsBox = await Hive.openBox(HiveBoxes.globalSettings); if (!globalSettingsBox.hasValue) { From 880695e04fe9db3b6976e5bbb898f26e088497b4 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 3 Jun 2023 15:20:20 +0200 Subject: [PATCH 2/4] fix: Fix scrolling bug on inbox page --- android/app/build.gradle | 2 +- .../view/document_search_page.dart | 10 +- .../view/sliver_search_bar.dart | 26 ++- .../documents/view/pages/documents_page.dart | 128 +++++++---- .../widgets/items/document_list_item.dart | 33 +-- lib/features/home/view/home_page.dart | 17 +- lib/features/inbox/cubit/inbox_cubit.dart | 15 +- lib/features/inbox/view/pages/inbox_page.dart | 202 +++++++++--------- .../inbox/view/widgets/inbox_item.dart | 139 +++++++++++- .../widgets/inbox_list_loading_widget.dart | 124 ----------- .../cubit/document_paging_bloc_mixin.dart | 18 +- .../example/ios/Podfile | 44 ++++ 12 files changed, 427 insertions(+), 331 deletions(-) delete mode 100644 lib/features/inbox/view/widgets/inbox_list_loading_widget.dart create mode 100644 packages/paperless_document_scanner/example/ios/Podfile diff --git a/android/app/build.gradle b/android/app/build.gradle index 20e446a..43710e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -70,7 +70,7 @@ android { } buildTypes { release { - signingConfig signingConfigs.release + signingConfig signingConfigs.debug } } diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 31b2d6b..f949891 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -108,8 +108,9 @@ class _DocumentSearchPageState extends State { } Widget _buildSuggestionsView(DocumentSearchState state) { - final suggestions = - state.suggestions.whereNot((element) => state.searchHistory.contains(element)).toList(); + final suggestions = state.suggestions + .whereNot((element) => state.searchHistory.contains(element)) + .toList(); final historyMatches = state.searchHistory .where( (element) => element.startsWith(query), @@ -140,7 +141,7 @@ class _DocumentSearchPageState extends State { childCount: suggestions.length, ), ), - if (suggestions.isEmpty && historyMatches.isEmpty) + if (suggestions.isEmpty && historyMatches.isEmpty && state.hasLoaded) SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverToBoxAdapter( @@ -191,7 +192,8 @@ class _DocumentSearchPageState extends State { builder: (context, state) { return ViewTypeSelectionWidget( viewType: state.viewType, - onChanged: (type) => context.read().updateViewType(type), + onChanged: (type) => + context.read().updateViewType(type), ); }, ) diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index 8e55376..4587ee7 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -16,18 +16,22 @@ class SliverSearchBar extends StatelessWidget { @override Widget build(BuildContext context) { - final currentUser = - Hive.box(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser; + final currentUser = Hive.box(HiveBoxes.globalSettings) + .getValue()! + .currentLoggedInUser; - return SliverPersistentHeader( - floating: floating, - pinned: pinned, - delegate: CustomizableSliverPersistentHeaderDelegate( - minExtent: kToolbarHeight, - maxExtent: kToolbarHeight, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0), - child: const DocumentSearchBar(), + return SliverPadding( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + sliver: SliverPersistentHeader( + floating: floating, + pinned: pinned, + delegate: CustomizableSliverPersistentHeaderDelegate( + minExtent: kToolbarHeight, + maxExtent: kToolbarHeight, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16.0), + child: const DocumentSearchBar(), + ), ), ), ); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 8bbdad7..ce3c98a 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -42,9 +42,12 @@ class DocumentsPage extends StatefulWidget { State createState() => _DocumentsPageState(); } -class _DocumentsPageState extends State with SingleTickerProviderStateMixin { - final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); - final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle(); +class _DocumentsPageState extends State + with SingleTickerProviderStateMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); + final SliverOverlapAbsorberHandle tabBarHandle = + SliverOverlapAbsorberHandle(); late final TabController _tabController; int _currentTab = 0; @@ -81,7 +84,8 @@ class _DocumentsPageState extends State with SingleTickerProvider @override Widget build(BuildContext context) { return BlocListener( - listenWhen: (previous, current) => !previous.isSuccess && current.isSuccess, + listenWhen: (previous, current) => + !previous.isSuccess && current.isSuccess, listener: (context, state) { showSnackBar( context, @@ -98,7 +102,8 @@ class _DocumentsPageState extends State with SingleTickerProvider }, child: BlocConsumer( listenWhen: (previous, current) => - previous != ConnectivityState.connected && current == ConnectivityState.connected, + previous != ConnectivityState.connected && + current == ConnectivityState.connected, listener: (context, state) { try { context.read().reload(); @@ -146,7 +151,11 @@ class _DocumentsPageState extends State with SingleTickerProvider resizeToAvoidBottomInset: true, body: WillPopScope( onWillPop: () async { - if (context.read().state.selection.isNotEmpty) { + if (context + .read() + .state + .selection + .isNotEmpty) { context.read().resetSelection(); return false; } @@ -161,18 +170,13 @@ class _DocumentsPageState extends State with SingleTickerProvider handle: searchBarHandle, sliver: BlocBuilder( builder: (context, state) { - return AnimatedSwitcher( - layoutBuilder: SliverAnimatedSwitcher.defaultLayoutBuilder, - transitionBuilder: SliverAnimatedSwitcher.defaultTransitionBuilder, - child: state.selection.isEmpty - ? const SliverSearchBar(floating: true) - : DocumentSelectionSliverAppBar( - state: state, - ), - duration: const Duration( - milliseconds: 250, - ), - ); + if (state.selection.isEmpty) { + return const SliverSearchBar(floating: true); + } else { + return DocumentSelectionSliverAppBar( + state: state, + ); + } }, ), ), @@ -187,7 +191,8 @@ class _DocumentsPageState extends State with SingleTickerProvider } return SliverPersistentHeader( pinned: true, - delegate: CustomizableSliverPersistentHeaderDelegate( + delegate: + CustomizableSliverPersistentHeaderDelegate( minExtent: kTextTabBarHeight, maxExtent: kTextTabBarHeight, child: ColoredTabBar( @@ -211,15 +216,22 @@ class _DocumentsPageState extends State with SingleTickerProvider if (metrics.maxScrollExtent == 0) { return true; } - final desiredTab = (metrics.pixels / metrics.maxScrollExtent).round(); - if (metrics.axis == Axis.horizontal && _currentTab != desiredTab) { + final desiredTab = + (metrics.pixels / metrics.maxScrollExtent) + .round(); + if (metrics.axis == Axis.horizontal && + _currentTab != desiredTab) { setState(() => _currentTab = desiredTab); } return false; }, child: TabBarView( controller: _tabController, - physics: context.watch().state.selection.isNotEmpty + physics: context + .watch() + .state + .selection + .isNotEmpty ? const NeverScrollableScrollPhysics() : null, children: [ @@ -287,13 +299,19 @@ class _DocumentsPageState extends State with SingleTickerProvider final currState = context.read().state; final max = notification.metrics.maxScrollExtent; - if (max == 0 || _currentTab != 0 || currState.isLoading || currState.isLastPageLoaded) { + if (max == 0 || + _currentTab != 0 || + currState.isLoading || + currState.isLastPageLoaded) { return false; } final offset = notification.metrics.pixels; if (offset >= max * 0.7) { - context.read().loadMore().onError( + context + .read() + .loadMore() + .onError( (error, stackTrace) => showErrorMessage( context, error, @@ -324,16 +342,20 @@ class _DocumentsPageState extends State with SingleTickerProvider ), ); } - + final allowToggleFilter = state.selection.isEmpty; return SliverAdaptiveDocumentsView( viewType: state.viewType, onTap: _openDetails, - onSelected: context.read().toggleDocumentSelection, + onSelected: + context.read().toggleDocumentSelection, hasInternetConnection: connectivityState.isConnected, - onTagSelected: _addTagToFilter, - onCorrespondentSelected: _addCorrespondentToFilter, - onDocumentTypeSelected: _addDocumentTypeToFilter, - onStoragePathSelected: _addStoragePathToFilter, + onTagSelected: allowToggleFilter ? _addTagToFilter : null, + onCorrespondentSelected: + allowToggleFilter ? _addCorrespondentToFilter : null, + onDocumentTypeSelected: + allowToggleFilter ? _addDocumentTypeToFilter : null, + onStoragePathSelected: + allowToggleFilter ? _addStoragePathToFilter : null, documents: state.documents, hasLoaded: state.hasLoaded, isLabelClickable: true, @@ -401,7 +423,8 @@ class _DocumentsPageState extends State with SingleTickerProvider snapSizes: const [0.9, 1], initialChildSize: .9, maxChildSize: 1, - builder: (context, controller) => BlocBuilder( + builder: (context, controller) => + BlocBuilder( builder: (context, state) { return DocumentFilterPanel( initialFilter: context.read().state.filter, @@ -422,7 +445,9 @@ class _DocumentsPageState extends State with SingleTickerProvider if (filterIntent.shouldReset) { await context.read().resetFilter(); } else { - await context.read().updateFilter(filter: filterIntent.filter!); + await context + .read() + .updateFilter(filter: filterIntent.filter!); } } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); @@ -439,7 +464,6 @@ class _DocumentsPageState extends State with SingleTickerProvider void _addTagToFilter(int tagId) { final cubit = context.read(); - try { cubit.state.filter.tags.maybeMap( ids: (state) { @@ -447,7 +471,9 @@ class _DocumentsPageState extends State with SingleTickerProvider cubit.updateCurrentFilter( (filter) => filter.copyWith( tags: state.copyWith( - include: state.include.whereNot((element) => element == tagId).toList(), + include: state.include + .whereNot((element) => element == tagId) + .toList(), ), ), ); @@ -455,7 +481,9 @@ class _DocumentsPageState extends State with SingleTickerProvider cubit.updateCurrentFilter( (filter) => filter.copyWith( tags: state.copyWith( - exclude: state.exclude.whereNot((element) => element == tagId).toList(), + exclude: state.exclude + .whereNot((element) => element == tagId) + .toList(), ), ), ); @@ -481,22 +509,26 @@ class _DocumentsPageState extends State with SingleTickerProvider void _addCorrespondentToFilter(int? correspondentId) { if (correspondentId == null) return; final cubit = context.read(); + try { cubit.state.filter.correspondent.maybeWhen( fromId: (id) { if (id == correspondentId) { cubit.updateCurrentFilter( - (filter) => filter.copyWith(correspondent: const IdQueryParameter.unset()), + (filter) => filter.copyWith( + correspondent: const IdQueryParameter.unset()), ); } else { cubit.updateCurrentFilter( - (filter) => filter.copyWith(correspondent: IdQueryParameter.fromId(correspondentId)), + (filter) => filter.copyWith( + correspondent: IdQueryParameter.fromId(correspondentId)), ); } }, orElse: () { cubit.updateCurrentFilter( - (filter) => filter.copyWith(correspondent: IdQueryParameter.fromId(correspondentId)), + (filter) => filter.copyWith( + correspondent: IdQueryParameter.fromId(correspondentId)), ); }, ); @@ -508,22 +540,26 @@ class _DocumentsPageState extends State with SingleTickerProvider void _addDocumentTypeToFilter(int? documentTypeId) { if (documentTypeId == null) return; final cubit = context.read(); + try { cubit.state.filter.documentType.maybeWhen( fromId: (id) { if (id == documentTypeId) { cubit.updateCurrentFilter( - (filter) => filter.copyWith(documentType: const IdQueryParameter.unset()), + (filter) => + filter.copyWith(documentType: const IdQueryParameter.unset()), ); } else { cubit.updateCurrentFilter( - (filter) => filter.copyWith(documentType: IdQueryParameter.fromId(documentTypeId)), + (filter) => filter.copyWith( + documentType: IdQueryParameter.fromId(documentTypeId)), ); } }, orElse: () { cubit.updateCurrentFilter( - (filter) => filter.copyWith(documentType: IdQueryParameter.fromId(documentTypeId)), + (filter) => filter.copyWith( + documentType: IdQueryParameter.fromId(documentTypeId)), ); }, ); @@ -535,22 +571,26 @@ class _DocumentsPageState extends State with SingleTickerProvider void _addStoragePathToFilter(int? pathId) { if (pathId == null) return; final cubit = context.read(); + try { cubit.state.filter.storagePath.maybeWhen( fromId: (id) { if (id == pathId) { cubit.updateCurrentFilter( - (filter) => filter.copyWith(storagePath: const IdQueryParameter.unset()), + (filter) => + filter.copyWith(storagePath: const IdQueryParameter.unset()), ); } else { cubit.updateCurrentFilter( - (filter) => filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)), + (filter) => + filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)), ); } }, orElse: () { cubit.updateCurrentFilter( - (filter) => filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)), + (filter) => + filter.copyWith(storagePath: IdQueryParameter.fromId(pathId)), ); }, ); 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 8ffdf3d..e187b14 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -81,40 +81,25 @@ class DocumentListItem extends DocumentItem { overflow: TextOverflow.ellipsis, text: TextSpan( text: DateFormat.yMMMd().format(document.created), - style: Theme.of(context).textTheme.labelSmall?.apply(color: Colors.grey), + style: Theme.of(context) + .textTheme + .labelSmall + ?.apply(color: Colors.grey), children: document.documentType != null ? [ const TextSpan(text: '\u30FB'), TextSpan( text: labels.documentTypes[document.documentType]?.name, - recognizer: TapGestureRecognizer() - ..onTap = () => onDocumentTypeSelected?.call(document.documentType), + recognizer: onDocumentTypeSelected != null + ? (TapGestureRecognizer() + ..onTap = () => onDocumentTypeSelected!( + document.documentType)) + : null, ), ] : null, ), ), - // Row( - // children: [ - // Text( - // DateFormat.yMMMd().format(document.created), - // style: Theme.of(context) - // .textTheme - // .bodySmall - // ?.apply(color: Colors.grey), - // ), - // if (document.documentType != null) ...[ - // Text("\u30FB"), - // DocumentTypeWidget( - // documentTypeId: document.documentType, - // textStyle: Theme.of(context).textTheme.bodySmall?.apply( - // color: Colors.grey, - // overflow: TextOverflow.ellipsis, - // ), - // ), - // ], - // ], - // ), ), isThreeLine: document.tags.isNotEmpty, leading: AspectRatio( diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b77e6cb..819d2b6 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -32,7 +32,8 @@ import 'package:responsive_builder/responsive_builder.dart'; /// Performs initialization logic. class HomePage extends StatefulWidget { final int paperlessApiVersion; - const HomePage({Key? key, required this.paperlessApiVersion}) : super(key: key); + const HomePage({Key? key, required this.paperlessApiVersion}) + : super(key: key); @override _HomePageState createState() => _HomePageState(); @@ -231,7 +232,8 @@ class _HomePageState extends State with WidgetsBindingObserver { listeners: [ BlocListener( // If app was started offline, load data once it comes back online. - listenWhen: (previous, current) => current == ConnectivityState.connected, + listenWhen: (previous, current) => + current == ConnectivityState.connected, listener: (context, state) { context.read().initialize(); context.read().initialize(); @@ -241,7 +243,9 @@ class _HomePageState extends State with WidgetsBindingObserver { listener: (context, state) { if (state.task != null) { // Handle local notifications on task change (only when app is running for now). - context.read().notifyTaskChanged(state.task!); + context + .read() + .notifyTaskChanged(state.task!); } }, ), @@ -254,7 +258,9 @@ class _HomePageState extends State with WidgetsBindingObserver { children: [ NavigationRail( labelType: NavigationRailLabelType.all, - destinations: destinations.map((e) => e.toNavigationRailDestination()).toList(), + destinations: destinations + .map((e) => e.toNavigationRailDestination()) + .toList(), selectedIndex: _currentIndex, onDestinationSelected: _onNavigationChanged, ), @@ -272,7 +278,8 @@ class _HomePageState extends State with WidgetsBindingObserver { elevation: 4.0, selectedIndex: _currentIndex, onDestinationSelected: _onNavigationChanged, - destinations: destinations.map((e) => e.toNavigationDestination()).toList(), + destinations: + destinations.map((e) => e.toNavigationDestination()).toList(), ), body: routes[_currentIndex], ); diff --git a/lib/features/inbox/cubit/inbox_cubit.dart b/lib/features/inbox/cubit/inbox_cubit.dart index 3a5cf89..3356fd5 100644 --- a/lib/features/inbox/cubit/inbox_cubit.dart +++ b/lib/features/inbox/cubit/inbox_cubit.dart @@ -13,7 +13,8 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/document_pag part 'inbox_cubit.g.dart'; part 'inbox_state.dart'; -class InboxCubit extends HydratedCubit with DocumentPagingBlocMixin { +class InboxCubit extends HydratedCubit + with DocumentPagingBlocMixin { final LabelRepository _labelRepository; final PaperlessDocumentsApi _documentsApi; @@ -38,7 +39,10 @@ class InboxCubit extends HydratedCubit with DocumentPagingBlocMixin this, onDeleted: remove, onUpdated: (document) { - if (document.tags.toSet().intersection(state.inboxTags.toSet()).isEmpty) { + if (document.tags + .toSet() + .intersection(state.inboxTags.toSet()) + .isEmpty) { remove(document); emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); } else { @@ -139,7 +143,8 @@ class InboxCubit extends HydratedCubit with DocumentPagingBlocMixin /// from the inbox. /// Future> removeFromInbox(DocumentModel document) async { - final tagsToRemove = document.tags.toSet().intersection(state.inboxTags.toSet()); + final tagsToRemove = + document.tags.toSet().intersection(state.inboxTags.toSet()); final updatedTags = {...document.tags}..removeAll(tagsToRemove); final updatedDocument = await api.update( @@ -193,8 +198,8 @@ class InboxCubit extends HydratedCubit with DocumentPagingBlocMixin Future assignAsn(DocumentModel document) async { if (document.archiveSerialNumber == null) { final int asn = await _documentsApi.findNextAsn(); - final updatedDocument = - await _documentsApi.update(document.copyWith(archiveSerialNumber: () => asn)); + final updatedDocument = await _documentsApi + .update(document.copyWith(archiveSerialNumber: () => asn)); replace(updatedDocument); } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 6d12ec8..0f09640 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -15,7 +15,6 @@ import 'package:paperless_mobile/features/document_search/view/sliver_search_bar 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'; -import 'package:paperless_mobile/features/inbox/view/widgets/inbox_list_loading_widget.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; @@ -27,18 +26,15 @@ class InboxPage extends StatefulWidget { State createState() => _InboxPageState(); } -class _InboxPageState extends State with DocumentPagingViewMixin { - final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); +class _InboxPageState extends State + with DocumentPagingViewMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); @override final pagingScrollController = ScrollController(); final _emptyStateRefreshIndicatorKey = GlobalKey(); - - @override - void initState() { - super.initState(); - context.read().reloadInbox(); - } + final _scrollController = ScrollController(); @override Widget build(BuildContext context) { @@ -63,98 +59,104 @@ class _InboxPageState extends State with DocumentPagingViewMixin( - builder: (context, state) { - return SafeArea( - top: true, - child: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - handle: searchBarHandle, - sliver: const SliverSearchBar(), - ) - ], - body: Builder( - builder: (context) { - if (!state.hasLoaded) { - return const InboxListLoadingWidget(); - } else if (state.documents.isEmpty) { - return Center( - child: InboxEmptyWidget( - emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, + body: SafeArea( + top: true, + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: const SliverSearchBar(), + ) + ], + body: BlocBuilder( + builder: (_, state) { + if (state.documents.isEmpty && state.hasLoaded) { + return Center( + child: InboxEmptyWidget( + emptyStateRefreshIndicatorKey: + _emptyStateRefreshIndicatorKey, + ), + ); + } else if (state.isLoading) { + return ListView.builder( + padding: const EdgeInsets.only(top: 16, left: 16), + controller: _scrollController, + itemBuilder: (context, index) { + return const InboxItemPlaceholder(); + }, + ); + } else { + return RefreshIndicator( + onRefresh: context.read().reload, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverToBoxAdapter( + child: HintCard( + show: !state.isHintAcknowledged, + hintText: + S.of(context)!.swipeLeftToMarkADocumentAsSeen, + onHintAcknowledged: () => + context.read().acknowledgeHint(), + ), ), - ); - } else { - return RefreshIndicator( - onRefresh: context.read().reload, - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: HintCard( - show: !state.isHintAcknowledged, - hintText: S.of(context)!.swipeLeftToMarkADocumentAsSeen, - onHintAcknowledged: () => - context.read().acknowledgeHint(), - ), - ), - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - ..._groupByDate(state.documents) - .entries - .map( - (entry) => [ - SliverToBoxAdapter( - child: Align( - alignment: Alignment.centerLeft, - child: ClipRRect( - borderRadius: BorderRadius.circular(32.0), - child: Text( - entry.key, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ).padded(), - ), - ).paddedOnly(top: 8.0), + // Build a list of slivers alternating between SliverToBoxAdapter + // (group header) and a SliverList (inbox items). + ..._groupByDate(state.documents) + .entries + .map( + (entry) => [ + SliverToBoxAdapter( + child: Align( + alignment: Alignment.centerLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Text( + entry.key, + style: + Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ).padded(), ), - SliverList( - delegate: SliverChildBuilderDelegate( - childCount: entry.value.length, - (context, index) { - if (index < entry.value.length - 1) { - return Column( - children: [ - _buildListItem( - entry.value[index], - ), - const Divider( - indent: 16, - endIndent: 16, - ), - ], - ); - } - return _buildListItem( - entry.value[index], - ); - }, - ), - ), - ], - ) - .flattened - .toList(), - const SliverToBoxAdapter( - child: SizedBox(height: 78), - ), - ], + ).paddedOnly(top: 8.0), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + childCount: entry.value.length, + (context, index) { + if (index < entry.value.length - 1) { + return Column( + children: [ + _buildListItem( + entry.value[index], + ), + const Divider( + indent: 16, + endIndent: 16, + ), + ], + ); + } + return _buildListItem( + entry.value[index], + ); + }, + ), + ), + ], + ) + .flattened + .toList(), + const SliverToBoxAdapter( + child: SizedBox(height: 78), ), - ); - } - }, - ), - ), - ); - }, + ], + ), + ); + } + }, + ), + ), ), ); } @@ -239,7 +241,9 @@ class _InboxPageState extends State with DocumentPagingViewMixin removedTags, ) async { try { - await context.read().undoRemoveFromInbox(document, removedTags); + await context + .read() + .undoRemoveFromInbox(document, removedTags); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index 23bf0a2..e4faac2 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -4,18 +4,130 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; +import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.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:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +class InboxItemPlaceholder extends StatelessWidget { + const InboxItemPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return ShimmerPlaceholder( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const TextPlaceholder(length: 150, fontSize: 12), + const SizedBox( + height: 16, + ), + SizedBox( + height: 200, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 150, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 120, + width: 90, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: const ColoredBox( + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + const Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Spacer(), + TextPlaceholder(length: 200, fontSize: 14), + Spacer(), + TextPlaceholder(length: 120, fontSize: 14), + SizedBox(height: 8), + TextPlaceholder(length: 170, fontSize: 14), + Spacer(), + TagsPlaceholder(count: 3, dense: true), + Spacer(), + ], + ), + ), + ], + ), + ), + SizedBox( + height: 50, + child: IntrinsicHeight( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: Row( + children: [ + const SizedBox( + width: 50, + height: 40, + child: ColoredBox( + color: Colors.white, + ), + ).padded(), + const VerticalDivider( + indent: 12, + endIndent: 12, + ), + SizedBox( + height: 40, + child: Row( + children: [ + Container( + width: 150, + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.white, + ), + ), + const SizedBox(width: 4), + Container( + width: 200, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.white, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + class InboxItem extends StatefulWidget { static const a4AspectRatio = 1 / 1.4142; - final DocumentModel document; const InboxItem({ super.key, @@ -70,10 +182,14 @@ class _InboxItemState extends State { _buildTextWithLeadingIcon( Icon( Icons.person_outline, - size: Theme.of(context).textTheme.bodyMedium?.fontSize, + size: Theme.of(context) + .textTheme + .bodyMedium + ?.fontSize, ), LabelText( - label: state.labels.correspondents[widget.document.correspondent], + label: state.labels.correspondents[ + widget.document.correspondent], style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", ), @@ -81,10 +197,14 @@ class _InboxItemState extends State { _buildTextWithLeadingIcon( Icon( Icons.description_outlined, - size: Theme.of(context).textTheme.bodyMedium?.fontSize, + size: Theme.of(context) + .textTheme + .bodyMedium + ?.fontSize, ), LabelText( - label: state.labels.documentTypes[widget.document.documentType], + label: state.labels.documentTypes[ + widget.document.documentType], style: Theme.of(context).textTheme.bodyMedium, placeholder: "-", ), @@ -139,8 +259,8 @@ class _InboxItemState extends State { onPressed: () async { final shouldDelete = await showDialog( context: context, - builder: (context) => - DeleteDocumentConfirmationDialog(document: widget.document), + builder: (context) => DeleteDocumentConfirmationDialog( + document: widget.document), ) ?? false; if (shouldDelete) { @@ -217,7 +337,10 @@ class _InboxItemState extends State { _isAsnAssignLoading = true; }); - context.read().assignAsn(widget.document).whenComplete( + context + .read() + .assignAsn(widget.document) + .whenComplete( () => setState(() => _isAsnAssignLoading = false), ); } diff --git a/lib/features/inbox/view/widgets/inbox_list_loading_widget.dart b/lib/features/inbox/view/widgets/inbox_list_loading_widget.dart deleted file mode 100644 index cdd89ea..0000000 --- a/lib/features/inbox/view/widgets/inbox_list_loading_widget.dart +++ /dev/null @@ -1,124 +0,0 @@ -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/tags_placeholder.dart'; -import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart'; - -class InboxListLoadingWidget extends StatelessWidget { - const InboxListLoadingWidget({super.key}); - - @override - Widget build(BuildContext context) { - return ListView.separated( - itemCount: 20, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _buildInboxItem().padded(), - separatorBuilder: (context, index) => const SizedBox(height: 16), - ).paddedOnly(top: 8); - } - - Widget _buildInboxItem() { - return ShimmerPlaceholder( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const TextPlaceholder(length: 150, fontSize: 12), - const SizedBox( - height: 16, - ), - SizedBox( - height: 200, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 150, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: 120, - width: 90, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: const ColoredBox( - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - const Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Spacer(), - TextPlaceholder(length: 200, fontSize: 14), - Spacer(), - TextPlaceholder(length: 120, fontSize: 14), - SizedBox(height: 8), - TextPlaceholder(length: 170, fontSize: 14), - Spacer(), - TagsPlaceholder(count: 3, dense: true), - Spacer(), - ], - ), - ), - ], - ), - ), - SizedBox( - height: 50, - child: IntrinsicHeight( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - child: Row( - children: [ - const SizedBox( - width: 50, - height: 40, - child: ColoredBox( - color: Colors.white, - ), - ).padded(), - const VerticalDivider( - indent: 12, - endIndent: 12, - ), - SizedBox( - height: 40, - child: Row( - children: [ - Container( - width: 150, - height: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Colors.white, - ), - ), - const SizedBox(width: 4), - Container( - width: 200, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Colors.white, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart b/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart index 05d87e3..13ceae5 100644 --- a/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart +++ b/lib/features/paged_document_view/cubit/document_paging_bloc_mixin.dart @@ -9,7 +9,8 @@ import 'paged_documents_state.dart'; /// Mixin which can be used on cubits that handle documents. /// This implements all paging and filtering logic. /// -mixin DocumentPagingBlocMixin on BlocBase { +mixin DocumentPagingBlocMixin + on BlocBase { PaperlessDocumentsApi get api; DocumentChangedNotifier get notifier; @@ -74,7 +75,7 @@ mixin DocumentPagingBlocMixin on BlocBase reload() async { - emit(state.copyWithPaged(isLoading: true)); + // emit(state.copyWithPaged(isLoading: true)); final filter = state.filter.copyWith(page: 1); try { final result = await api.findAll(filter); @@ -128,7 +129,8 @@ mixin DocumentPagingBlocMixin on BlocBase element.id == document.id), + results: foundPage.results + ..removeWhere((element) => element.id == document.id), ); final newCount = foundPage.count - 1; emit( @@ -136,7 +138,8 @@ mixin DocumentPagingBlocMixin on BlocBase - (currIndex == index ? replacementPage : element).copyWith(count: newCount), + (currIndex == index ? replacementPage : element) + .copyWith(count: newCount), ) .toList(), ), @@ -159,11 +162,14 @@ mixin DocumentPagingBlocMixin on BlocBase doc.id == document.id ? document : doc).toList(), + results: foundPage.results + .map((doc) => doc.id == document.id ? document : doc) + .toList(), ); final newState = state.copyWithPaged( value: state.value - .mapIndexed((currIndex, element) => currIndex == pageIndex ? replacementPage : element) + .mapIndexed((currIndex, element) => + currIndex == pageIndex ? replacementPage : element) .toList(), ); emit(newState); diff --git a/packages/paperless_document_scanner/example/ios/Podfile b/packages/paperless_document_scanner/example/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/packages/paperless_document_scanner/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end From 2f25a948ee2ac3c5ea8b0149ead057ec78f77d9e Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 3 Jun 2023 15:39:27 +0200 Subject: [PATCH 3/4] feat: Add debug output for label repository calls --- lib/core/repository/label_repository.dart | 68 ++++--- lib/features/home/view/home_page.dart | 24 ++- .../labels/view/pages/labels_page.dart | 172 ++++++++++++------ .../lib/src/models/document_filter.dart | 10 +- 4 files changed, 186 insertions(+), 88 deletions(-) diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index 6b5e7c6..08899bb 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -10,21 +10,26 @@ class LabelRepository extends PersistentRepository { LabelRepository(this._api) : super(const LabelRepositoryState()); - Future initialize() { - debugPrint("Initializing labels..."); - return Future.wait([ - findAllCorrespondents(), - findAllDocumentTypes(), - findAllStoragePaths(), - findAllTags(), - ]).catchError((error) { - debugPrint(error.toString()); - }, test: (error) => false); + Future initialize() async { + debugPrint("[LabelRepository] initialize() called."); + try { + await Future.wait([ + findAllCorrespondents(), + findAllDocumentTypes(), + findAllStoragePaths(), + findAllTags(), + ]); + } catch (error, stackTrace) { + debugPrint( + "[LabelRepository] An error occurred in initialize(): ${error.toString()}"); + debugPrintStack(stackTrace: stackTrace); + } } Future createTag(Tag object) async { final created = await _api.saveTag(object); - final updatedState = {...state.tags}..putIfAbsent(created.id!, () => created); + final updatedState = {...state.tags} + ..putIfAbsent(created.id!, () => created); emit(state.copyWith(tags: updatedState)); return created; } @@ -48,7 +53,8 @@ class LabelRepository extends PersistentRepository { Future> findAllTags([Iterable? ids]) async { final tags = await _api.getTags(ids); - final updatedState = {...state.tags}..addEntries(tags.map((e) => MapEntry(e.id!, e))); + final updatedState = {...state.tags} + ..addEntries(tags.map((e) => MapEntry(e.id!, e))); emit(state.copyWith(tags: updatedState)); return tags; } @@ -62,14 +68,16 @@ class LabelRepository extends PersistentRepository { Future createCorrespondent(Correspondent correspondent) async { final created = await _api.saveCorrespondent(correspondent); - final updatedState = {...state.correspondents}..putIfAbsent(created.id!, () => created); + final updatedState = {...state.correspondents} + ..putIfAbsent(created.id!, () => created); emit(state.copyWith(correspondents: updatedState)); return created; } Future deleteCorrespondent(Correspondent correspondent) async { await _api.deleteCorrespondent(correspondent); - final updatedState = {...state.correspondents}..removeWhere((k, v) => k == correspondent.id); + final updatedState = {...state.correspondents} + ..removeWhere((k, v) => k == correspondent.id); emit(state.copyWith(correspondents: updatedState)); return correspondent.id!; @@ -86,22 +94,22 @@ class LabelRepository extends PersistentRepository { return null; } - Future> findAllCorrespondents([Iterable? ids]) async { + Future> findAllCorrespondents( + [Iterable? ids]) async { debugPrint("Loading correspondents..."); final correspondents = await _api.getCorrespondents(ids); debugPrint("${correspondents.length} correspondents successfully loaded."); final updatedState = { ...state.correspondents, }..addAll({for (var element in correspondents) element.id!: element}); - debugPrint("Pushing new correspondents state."); emit(state.copyWith(correspondents: updatedState)); - debugPrint("New correspondents state pushed."); return correspondents; } Future updateCorrespondent(Correspondent correspondent) async { final updated = await _api.updateCorrespondent(correspondent); - final updatedState = {...state.correspondents}..update(updated.id!, (_) => updated); + final updatedState = {...state.correspondents} + ..update(updated.id!, (_) => updated); emit(state.copyWith(correspondents: updatedState)); return updated; @@ -109,14 +117,16 @@ class LabelRepository extends PersistentRepository { Future createDocumentType(DocumentType documentType) async { final created = await _api.saveDocumentType(documentType); - final updatedState = {...state.documentTypes}..putIfAbsent(created.id!, () => created); + final updatedState = {...state.documentTypes} + ..putIfAbsent(created.id!, () => created); emit(state.copyWith(documentTypes: updatedState)); return created; } Future deleteDocumentType(DocumentType documentType) async { await _api.deleteDocumentType(documentType); - final updatedState = {...state.documentTypes}..removeWhere((k, v) => k == documentType.id); + final updatedState = {...state.documentTypes} + ..removeWhere((k, v) => k == documentType.id); emit(state.copyWith(documentTypes: updatedState)); return documentType.id!; } @@ -131,7 +141,8 @@ class LabelRepository extends PersistentRepository { return null; } - Future> findAllDocumentTypes([Iterable? ids]) async { + Future> findAllDocumentTypes( + [Iterable? ids]) async { final documentTypes = await _api.getDocumentTypes(ids); final updatedState = {...state.documentTypes} ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); @@ -141,21 +152,24 @@ class LabelRepository extends PersistentRepository { Future updateDocumentType(DocumentType documentType) async { final updated = await _api.updateDocumentType(documentType); - final updatedState = {...state.documentTypes}..update(updated.id!, (_) => updated); + final updatedState = {...state.documentTypes} + ..update(updated.id!, (_) => updated); emit(state.copyWith(documentTypes: updatedState)); return updated; } Future createStoragePath(StoragePath storagePath) async { final created = await _api.saveStoragePath(storagePath); - final updatedState = {...state.storagePaths}..putIfAbsent(created.id!, () => created); + final updatedState = {...state.storagePaths} + ..putIfAbsent(created.id!, () => created); emit(state.copyWith(storagePaths: updatedState)); return created; } Future deleteStoragePath(StoragePath storagePath) async { await _api.deleteStoragePath(storagePath); - final updatedState = {...state.storagePaths}..removeWhere((k, v) => k == storagePath.id); + final updatedState = {...state.storagePaths} + ..removeWhere((k, v) => k == storagePath.id); emit(state.copyWith(storagePaths: updatedState)); return storagePath.id!; } @@ -170,7 +184,8 @@ class LabelRepository extends PersistentRepository { return null; } - Future> findAllStoragePaths([Iterable? ids]) async { + Future> findAllStoragePaths( + [Iterable? ids]) async { final storagePaths = await _api.getStoragePaths(ids); final updatedState = {...state.storagePaths} ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); @@ -180,7 +195,8 @@ class LabelRepository extends PersistentRepository { Future updateStoragePath(StoragePath storagePath) async { final updated = await _api.updateStoragePath(storagePath); - final updatedState = {...state.storagePaths}..update(updated.id!, (_) => updated); + final updatedState = {...state.storagePaths} + ..update(updated.id!, (_) => updated); emit(state.copyWith(storagePaths: updatedState)); return updated; } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 819d2b6..40aa42b 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -233,10 +233,28 @@ class _HomePageState extends State with WidgetsBindingObserver { BlocListener( // If app was started offline, load data once it comes back online. listenWhen: (previous, current) => + previous != ConnectivityState.connected && current == ConnectivityState.connected, - listener: (context, state) { - context.read().initialize(); - context.read().initialize(); + listener: (context, state) async { + try { + debugPrint( + "[HomePage] BlocListener#listener: " + "Loading saved views and labels...", + ); + await Future.wait([ + context.read().initialize(), + context.read().initialize(), + ]); + debugPrint("[HomePage] BlocListener#listener: " + "Saved views and labels successfully loaded."); + } catch (error, stackTrace) { + debugPrint( + '[HomePage] BlocListener.listener: ' + 'An error occurred while loading saved views and labels.\n' + '${error.toString()}', + ); + debugPrintStack(stackTrace: stackTrace); + } }, ), BlocListener( diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 29d1d97..4ebcf2f 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -29,9 +29,12 @@ class LabelsPage extends StatefulWidget { State createState() => _LabelsPageState(); } -class _LabelsPageState extends State with SingleTickerProviderStateMixin { - final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); - final SliverOverlapAbsorberHandle tabBarHandle = SliverOverlapAbsorberHandle(); +class _LabelsPageState extends State + with SingleTickerProviderStateMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); + final SliverOverlapAbsorberHandle tabBarHandle = + SliverOverlapAbsorberHandle(); late final TabController _tabController; int _currentIndex = 0; @@ -81,25 +84,33 @@ class _LabelsPageState extends State with SingleTickerProviderStateM Tab( icon: Icon( Icons.person_outline, - color: Theme.of(context).colorScheme.onPrimaryContainer, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, ), ), Tab( icon: Icon( Icons.description_outlined, - color: Theme.of(context).colorScheme.onPrimaryContainer, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, ), ), Tab( icon: Icon( Icons.label_outline, - color: Theme.of(context).colorScheme.onPrimaryContainer, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, ), ), Tab( icon: Icon( Icons.folder_open, - color: Theme.of(context).colorScheme.onPrimaryContainer, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, ), ), ], @@ -118,25 +129,44 @@ class _LabelsPageState extends State with SingleTickerProviderStateM if (metrics.maxScrollExtent == 0) { return true; } - final desiredTab = ((metrics.pixels / metrics.maxScrollExtent) * - (_tabController.length - 1)) - .round(); + final desiredTab = + ((metrics.pixels / metrics.maxScrollExtent) * + (_tabController.length - 1)) + .round(); - if (metrics.axis == Axis.horizontal && _currentIndex != desiredTab) { + if (metrics.axis == Axis.horizontal && + _currentIndex != desiredTab) { setState(() => _currentIndex = desiredTab); } return true; }, child: RefreshIndicator( edgeOffset: kTextTabBarHeight, - notificationPredicate: (notification) => connectedState.isConnected, - onRefresh: () => [ - context.read().reloadCorrespondents, - context.read().reloadDocumentTypes, - context.read().reloadTags, - context.read().reloadStoragePaths, - ][_currentIndex] - .call(), + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () async { + try { + await [ + context.read().reloadCorrespondents, + context.read().reloadDocumentTypes, + context.read().reloadTags, + context.read().reloadStoragePaths, + ][_currentIndex] + .call(); + } catch (error, stackTrace) { + debugPrint( + "[LabelsPage] RefreshIndicator.onRefresh " + "${[ + "correspondents", + "document types", + "tags", + "storage paths" + ][_currentIndex]}: " + "An error occurred (${error.toString()})", + ); + debugPrintStack(stackTrace: stackTrace); + } + }, child: TabBarView( controller: _tabController, children: [ @@ -144,22 +174,29 @@ class _LabelsPageState extends State with SingleTickerProviderStateM builder: (context) { return CustomScrollView( slivers: [ - SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector( + handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.correspondents, filterBuilder: (label) => DocumentFilter( - correspondent: IdQueryParameter.fromId(label.id!), + correspondent: + IdQueryParameter.fromId(label.id!), ), - canEdit: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.change, PermissionTarget.correspondent), - canAddNew: LocalUserAccount.current.paperlessUser + canEdit: LocalUserAccount + .current.paperlessUser .hasPermission( - PermissionAction.add, PermissionTarget.correspondent), + PermissionAction.change, + PermissionTarget.correspondent), + canAddNew: LocalUserAccount + .current.paperlessUser + .hasPermission(PermissionAction.add, + PermissionTarget.correspondent), onEdit: _openEditCorrespondentPage, emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent, - emptyStateDescription: S.of(context)!.noCorrespondentsSetUp, + emptyStateDescription: + S.of(context)!.noCorrespondentsSetUp, onAddNew: _openAddCorrespondentPage, ), ], @@ -170,22 +207,29 @@ class _LabelsPageState extends State with SingleTickerProviderStateM builder: (context) { return CustomScrollView( slivers: [ - SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector( + handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.documentTypes, filterBuilder: (label) => DocumentFilter( - documentType: IdQueryParameter.fromId(label.id!), + documentType: + IdQueryParameter.fromId(label.id!), ), - canEdit: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.change, PermissionTarget.documentType), - canAddNew: LocalUserAccount.current.paperlessUser + canEdit: LocalUserAccount + .current.paperlessUser .hasPermission( - PermissionAction.add, PermissionTarget.documentType), + PermissionAction.change, + PermissionTarget.documentType), + canAddNew: LocalUserAccount + .current.paperlessUser + .hasPermission(PermissionAction.add, + PermissionTarget.documentType), onEdit: _openEditDocumentTypePage, emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType, - emptyStateDescription: S.of(context)!.noDocumentTypesSetUp, + emptyStateDescription: + S.of(context)!.noDocumentTypesSetUp, onAddNew: _openAddDocumentTypePage, ), ], @@ -196,18 +240,24 @@ class _LabelsPageState extends State with SingleTickerProviderStateM builder: (context) { return CustomScrollView( slivers: [ - SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector( + handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.tags, filterBuilder: (label) => DocumentFilter( - tags: TagsQuery.ids(include: [label.id!]), + tags: + TagsQuery.ids(include: [label.id!]), ), - canEdit: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.change, PermissionTarget.tag), - canAddNew: LocalUserAccount.current.paperlessUser + canEdit: LocalUserAccount + .current.paperlessUser .hasPermission( - PermissionAction.add, PermissionTarget.tag), + PermissionAction.change, + PermissionTarget.tag), + canAddNew: LocalUserAccount + .current.paperlessUser + .hasPermission(PermissionAction.add, + PermissionTarget.tag), onEdit: _openEditTagPage, leadingBuilder: (t) => CircleAvatar( backgroundColor: t.color, @@ -218,8 +268,10 @@ class _LabelsPageState extends State with SingleTickerProviderStateM ) : null, ), - emptyStateActionButtonLabel: S.of(context)!.addNewTag, - emptyStateDescription: S.of(context)!.noTagsSetUp, + emptyStateActionButtonLabel: + S.of(context)!.addNewTag, + emptyStateDescription: + S.of(context)!.noTagsSetUp, onAddNew: _openAddTagPage, ), ], @@ -230,22 +282,30 @@ class _LabelsPageState extends State with SingleTickerProviderStateM builder: (context) { return CustomScrollView( slivers: [ - SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector( + handle: searchBarHandle), SliverOverlapInjector(handle: tabBarHandle), LabelTabView( labels: state.storagePaths, onEdit: _openEditStoragePathPage, filterBuilder: (label) => DocumentFilter( - storagePath: IdQueryParameter.fromId(label.id!), + storagePath: + IdQueryParameter.fromId(label.id!), ), - canEdit: LocalUserAccount.current.paperlessUser.hasPermission( - PermissionAction.change, PermissionTarget.storagePath), - canAddNew: LocalUserAccount.current.paperlessUser + canEdit: LocalUserAccount + .current.paperlessUser .hasPermission( - PermissionAction.add, PermissionTarget.storagePath), + PermissionAction.change, + PermissionTarget.storagePath), + canAddNew: LocalUserAccount + .current.paperlessUser + .hasPermission(PermissionAction.add, + PermissionTarget.storagePath), contentBuilder: (path) => Text(path.path), - emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath, - emptyStateDescription: S.of(context)!.noStoragePathsSetUp, + emptyStateActionButtonLabel: + S.of(context)!.addNewStoragePath, + emptyStateDescription: + S.of(context)!.noStoragePathsSetUp, onAddNew: _openAddStoragePathPage, ), ], @@ -326,13 +386,13 @@ class _LabelsPageState extends State with SingleTickerProviderStateM MaterialPageRoute _buildLabelPageRoute(Widget page) { return MaterialPageRoute( - builder: (_) => MultiProvider( - providers: [ - Provider.value(value: context.read()), - Provider.value(value: context.read()) - ], - child: page - ) + builder: (_) => MultiProvider( + providers: [ + Provider.value(value: context.read()), + Provider.value(value: context.read()) + ], + child: page, + ), ); } } diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index be89909..6da3db9 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -115,7 +115,9 @@ class DocumentFilter extends Equatable { final queryParams = groupBy(params, (e) => e.key).map( (key, entries) => MapEntry( key, - entries.length == 1 ? entries.first.value : entries.map((e) => e.value).join(","), + entries.length == 1 + ? entries.first.value + : entries.map((e) => e.value).join(","), ), ); return queryParams; @@ -156,7 +158,8 @@ class DocumentFilter extends Equatable { modified: modified ?? this.modified, moreLike: moreLike != null ? moreLike.call() : this.moreLike, ); - if (query?.queryType != QueryType.extended && newFilter.forceExtendedQuery) { + if (query?.queryType != QueryType.extended && + newFilter.forceExtendedQuery) { //Prevents infinite recursion return newFilter.copyWith( query: newFilter.query.copyWith(queryType: QueryType.extended), @@ -212,7 +215,8 @@ class DocumentFilter extends Equatable { query, ]; - factory DocumentFilter.fromJson(Map json) => _$DocumentFilterFromJson(json); + factory DocumentFilter.fromJson(Map json) => + _$DocumentFilterFromJson(json); Map toJson() => _$DocumentFilterToJson(this); } From 9d4b13cdd68734186635292e1c27afa18a455545 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sat, 3 Jun 2023 15:56:50 +0200 Subject: [PATCH 4/4] chore: formatting --- lib/features/inbox/view/pages/inbox_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 0f09640..37fad1d 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -89,7 +89,6 @@ class _InboxPageState extends State return RefreshIndicator( onRefresh: context.read().reload, child: CustomScrollView( - controller: _scrollController, slivers: [ SliverToBoxAdapter( child: HintCard(