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