From a8a41b38a8db18712f3206f1844186420711d45e Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 22 Feb 2023 18:14:02 +0100 Subject: [PATCH] feat: Update translations, fix scrolling on all pages --- .gitignore | 4 +- ...ble_sliver_persistent_header_delegate.dart | 27 ++ .../document_scan/view/scanner_page.dart | 151 +++--- .../view/sliver_search_bar.dart | 59 +++ .../documents/view/pages/documents_page.dart | 433 +++++++++--------- .../document_selection_sliver_app_bar.dart | 55 +++ lib/features/inbox/view/pages/inbox_page.dart | 186 ++++---- .../labels/view/pages/labels_page.dart | 407 ++++++++-------- .../labels/view/widgets/label_text.dart | 2 + lib/l10n/intl_cs.arb | 2 +- lib/l10n/intl_de.arb | 4 +- lib/l10n/intl_fr.arb | 176 +++---- lib/l10n/intl_pl.arb | 4 +- 13 files changed, 813 insertions(+), 697 deletions(-) create mode 100644 lib/core/delegate/customizable_sliver_persistent_header_delegate.dart create mode 100644 lib/features/document_search/view/sliver_search_bar.dart create mode 100644 lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart diff --git a/.gitignore b/.gitignore index 4a79f00..1a1c827 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,6 @@ untranslated_messages.txt #lakos generated files **/dot_images/* -docker/ \ No newline at end of file +docker/ + +crowdin_credentials.yml \ No newline at end of file diff --git a/lib/core/delegate/customizable_sliver_persistent_header_delegate.dart b/lib/core/delegate/customizable_sliver_persistent_header_delegate.dart new file mode 100644 index 0000000..07677fd --- /dev/null +++ b/lib/core/delegate/customizable_sliver_persistent_header_delegate.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class CustomizableSliverPersistentHeaderDelegate + extends SliverPersistentHeaderDelegate { + @override + final double minExtent; + @override + final double maxExtent; + final Widget child; + + CustomizableSliverPersistentHeaderDelegate({ + required this.child, + required this.minExtent, + required this.maxExtent, + }); + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return child; + } + + @override + bool shouldRebuild(CustomizableSliverPersistentHeaderDelegate oldDelegate) { + return false; + } +} diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index 0d80406..a63721a 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; @@ -16,6 +17,7 @@ import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; @@ -42,17 +44,13 @@ class ScannerPage extends StatefulWidget { class _ScannerPageState extends State with SingleTickerProviderStateMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); + final SliverOverlapAbsorberHandle actionsHandle = + SliverOverlapAbsorberHandle(); + @override Widget build(BuildContext context) { - final safeAreaPadding = MediaQuery.of(context).padding; - final availableHeight = MediaQuery.of(context).size.height - - 2 * kToolbarHeight - - kTextTabBarHeight - - kBottomNavigationBarHeight - - safeAreaPadding.top - - safeAreaPadding.bottom; - - print(availableHeight); return BlocBuilder( builder: (context, connectedState) { return Scaffold( @@ -68,54 +66,49 @@ class _ScannerPageState extends State // ), body: BlocBuilder>( builder: (context, state) { - return CustomScrollView( - physics: - state.isEmpty ? const NeverScrollableScrollPhysics() : null, - slivers: [ - SearchAppBar( - hintText: S.of(context)!.searchDocuments, - onOpenSearch: showDocumentSearchPage, - bottom: PreferredSize( - child: _buildActions(connectedState.isConnected), - preferredSize: const Size.fromHeight(kTextTabBarHeight), - ), + return SafeArea( + child: Scaffold( + drawer: const AppDrawer(), + floatingActionButton: FloatingActionButton( + onPressed: () => _openDocumentScanner(context), + child: const Icon(Icons.add_a_photo_outlined), ), - if (state.isEmpty) - SliverToBoxAdapter( - child: SizedBox( - height: availableHeight, - child: Center( - child: _buildEmptyState(connectedState.isConnected), + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: const SliverSearchBar(), + ), + SliverOverlapAbsorber( + handle: actionsHandle, + sliver: SliverPersistentHeader( + pinned: true, + delegate: CustomizableSliverPersistentHeaderDelegate( + child: _buildActions(connectedState.isConnected), + maxExtent: kTextTabBarHeight, + minExtent: kTextTabBarHeight, + ), ), ), - ) - else - _buildImageGrid(state) - ], - ); - - NestedScrollView( - floatHeaderSlivers: false, - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SearchAppBar( - hintText: S.of(context)!.searchDocuments, - onOpenSearch: showDocumentSearchPage, - bottom: PreferredSize( - child: _buildActions(connectedState.isConnected), - preferredSize: const Size.fromHeight(kTextTabBarHeight), + ], + body: BlocBuilder>( + builder: (context, state) { + if (state.isEmpty) { + return SizedBox.expand( + child: Center( + child: _buildEmptyState( + connectedState.isConnected, + state, + ), + ), + ); + } else { + return _buildImageGrid(state); + } + }, ), ), - ], - body: CustomScrollView( - slivers: [ - if (state.isEmpty) - SliverFillViewport( - delegate: SliverChildListDelegate.fixed( - [_buildEmptyState(connectedState.isConnected)]), - ) - else - _buildImageGrid(state) - ], ), ); }, @@ -237,36 +230,32 @@ class _ScannerPageState extends State } } - Widget _buildEmptyState(bool isConnected) { - return BlocBuilder>( - builder: (context, scans) { - if (scans.isNotEmpty) { - return _buildImageGrid(scans); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.of(context)!.noDocumentsScannedYet, - textAlign: TextAlign.center, - ), - TextButton( - child: Text(S.of(context)!.scanADocument), - onPressed: () => _openDocumentScanner(context), - ), - Text(S.of(context)!.or), - TextButton( - child: Text(S.of(context)!.uploadADocumentFromThisDevice), - onPressed: isConnected ? _onUploadFromFilesystem : null, - ), - ], + Widget _buildEmptyState(bool isConnected, List scans) { + if (scans.isNotEmpty) { + return _buildImageGrid(scans); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context)!.noDocumentsScannedYet, + textAlign: TextAlign.center, ), - ), - ); - }, + TextButton( + child: Text(S.of(context)!.scanADocument), + onPressed: () => _openDocumentScanner(context), + ), + Text(S.of(context)!.or), + TextButton( + child: Text(S.of(context)!.uploadADocumentFromThisDevice), + onPressed: isConnected ? _onUploadFromFilesystem : null, + ), + ], + ), + ), ); } diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart new file mode 100644 index 0000000..706bbe8 --- /dev/null +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; +import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; +import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; +import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'; +import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class SliverSearchBar extends StatelessWidget { + final bool floating; + final bool pinned; + const SliverSearchBar({ + super.key, + this.floating = false, + this.pinned = false, + }); + + @override + Widget build(BuildContext context) { + return SliverPersistentHeader( + floating: floating, + pinned: pinned, + delegate: CustomizableSliverPersistentHeaderDelegate( + minExtent: 56 + 8, + maxExtent: 56 + 8, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SearchBar( + height: 56, + supportingText: S.of(context)!.searchDocuments, + onTap: () => showDocumentSearchPage(context), + leadingIcon: IconButton( + icon: const Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + trailingIcon: IconButton( + icon: BlocBuilder( + builder: (context, state) { + return CircleAvatar( + child: Text(state.information?.userInitials ?? ''), + ); + }, + ), + onPressed: () { + showDialog( + context: context, + builder: (context) => const AccountSettingsDialog(), + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index b094724..d5cd067 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -3,25 +3,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; -import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart'; -import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; -import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; - import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/routes/document_details_route.dart'; @@ -44,18 +44,14 @@ class DocumentsPage extends StatefulWidget { } class _DocumentsPageState extends State - with - SingleTickerProviderStateMixin, - DocumentPagingViewMixin { + with SingleTickerProviderStateMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); + final SliverOverlapAbsorberHandle tabBarHandle = + SliverOverlapAbsorberHandle(); late final TabController _tabController; - @override - ScrollController get pagingScrollController => - _nestedScrollViewKey.currentState?.innerController ?? ScrollController(); - - final GlobalKey _nestedScrollViewKey = GlobalKey(); int _currentTab = 0; - bool _showBackToTopButton = false; @override void initState() { @@ -73,32 +69,13 @@ class _DocumentsPageState extends State return []; }, ); - _tabController.addListener(_tabChangesListener); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _nestedScrollViewKey.currentState!.innerController - ..addListener(_scrollExtentListener) - ..addListener(shouldLoadMoreDocumentsListener); - }); } void _tabChangesListener() { setState(() => _currentTab = _tabController.index); } - void _scrollExtentListener() { - if (pagingScrollController.position.pixels > - MediaQuery.of(context).size.height) { - if (!_showBackToTopButton) { - setState(() => _showBackToTopButton = true); - } - } else { - if (_showBackToTopButton) { - setState(() => _showBackToTopButton = false); - } - } - } - @override void dispose() { _tabController.dispose(); @@ -136,135 +113,145 @@ class _DocumentsPageState extends State } }, builder: (context, connectivityState) { - return Scaffold( - drawer: const AppDrawer(), - floatingActionButton: BlocBuilder( - builder: (context, state) { - final appliedFiltersCount = state.filter.appliedFiltersCount; - return b.Badge( - position: b.BadgePosition.topEnd(top: -12, end: -6), - showBadge: appliedFiltersCount > 0, - badgeContent: Text( - '$appliedFiltersCount', - style: const TextStyle( - color: Colors.white, + return SafeArea( + child: Scaffold( + drawer: const AppDrawer(), + floatingActionButton: BlocBuilder( + builder: (context, state) { + final appliedFiltersCount = state.filter.appliedFiltersCount; + return b.Badge( + position: b.BadgePosition.topEnd(top: -12, end: -6), + showBadge: appliedFiltersCount > 0, + badgeContent: Text( + '$appliedFiltersCount', + style: const TextStyle( + color: Colors.white, + ), ), - ), - animationType: b.BadgeAnimationType.fade, - badgeColor: Colors.red, - child: _currentTab == 0 - ? FloatingActionButton( - child: const Icon(Icons.filter_alt_outlined), - onPressed: _openDocumentFilter, - ) - : FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () => _onCreateSavedView(state.filter), + animationType: b.BadgeAnimationType.fade, + badgeColor: Colors.red, + child: _currentTab == 0 + ? FloatingActionButton( + child: const Icon(Icons.filter_alt_outlined), + onPressed: _openDocumentFilter, + ) + : FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => _onCreateSavedView(state.filter), + ), + ); + }, + ), + resizeToAvoidBottomInset: true, + body: WillPopScope( + onWillPop: () async { + if (context + .read() + .state + .selection + .isNotEmpty) { + context.read().resetSelection(); + } + return false; + }, + child: Stack( + children: [ + NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: BlocBuilder( + builder: (context, state) { + if (state.selection.isNotEmpty) { + // Show selection app bar when selection mode is active + return DocumentSelectionSliverAppBar( + state: state); + } + return const SliverSearchBar(floating: true); + }, + ), ), - ); - }, - ), - resizeToAvoidBottomInset: true, - body: WillPopScope( - onWillPop: () async { - if (context.read().state.selection.isNotEmpty) { - context.read().resetSelection(); - } - return false; - }, - child: Stack( - children: [ - NestedScrollView( - key: _nestedScrollViewKey, - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - // This widget takes the overlapping behavior of the SliverAppBar, - // and redirects it to the SliverOverlapInjector below. If it is - // missing, then it is possible for the nested "inner" scroll view - // below to end up under the SliverAppBar even when the inner - // scroll view thinks it has not been scrolled. - // This is not necessary if the "headerSliverBuilder" only builds - // widgets that do not overlap the next sliver. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, - ), - sliver: BlocBuilder( - builder: (context, state) { - if (state.selection.isNotEmpty) { - return SliverAppBar( - floating: false, + SliverOverlapAbsorber( + handle: tabBarHandle, + sliver: BlocBuilder( + builder: (context, state) { + if (state.selection.isNotEmpty) { + return const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); + } + return SliverPersistentHeader( pinned: true, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => context - .read() - .resetSelection(), - ), - title: Text( - "${state.selection.length} ${S.of(context)!.countSelected}", - ), - actions: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _onDelete(state), + delegate: + CustomizableSliverPersistentHeaderDelegate( + minExtent: kTextTabBarHeight, + maxExtent: kTextTabBarHeight, + child: ColoredTabBar( + backgroundColor: Theme.of(context) + .colorScheme + .background, + tabBar: TabBar( + controller: _tabController, + tabs: [ + Tab(text: S.of(context)!.documents), + Tab(text: S.of(context)!.views), + ], + ), ), - ], + ), ); - } - return SearchAppBar( - hintText: S.of(context)!.searchDocuments, - onOpenSearch: showDocumentSearchPage, - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: S.of(context)!.documents), - Tab(text: S.of(context)!.views), - ], - ), - ); - }, + }, + ), + ), + ], + body: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } + final desiredTab = + (metrics.pixels / metrics.maxScrollExtent) + .round(); + if (metrics.axis == Axis.horizontal && + _currentTab != desiredTab) { + setState(() => _currentTab = desiredTab); + } + return false; + }, + child: TabBarView( + controller: _tabController, + physics: context + .watch() + .state + .selection + .isNotEmpty + ? const NeverScrollableScrollPhysics() + : null, + children: [ + Builder( + builder: (context) { + return _buildDocumentsTab( + connectivityState, + context, + ); + }, + ), + Builder( + builder: (context) { + return _buildSavedViewsTab( + connectivityState, + context, + ); + }, + ), + ], ), ), - ], - body: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.maxScrollExtent == 0) { - return true; - } - final desiredTab = - (metrics.pixels / metrics.maxScrollExtent).round(); - if (metrics.axis == Axis.horizontal && - _currentTab != desiredTab) { - setState(() => _currentTab = desiredTab); - } - return false; - }, - child: TabBarView( - controller: _tabController, - children: [ - Builder( - builder: (context) { - return _buildDocumentsTab( - connectivityState, - context, - ); - }, - ), - Builder( - builder: (context) { - return _buildSavedViewsTab( - connectivityState, - context, - ); - }, - ), - ], - ), ), - ), - if (_showBackToTopButton) _buildBackToTopAction(context), - ], + ], + ), ), ), ); @@ -273,53 +260,22 @@ class _DocumentsPageState extends State ); } - Widget _buildBackToTopAction(BuildContext context) { - return Transform.translate( - offset: const Offset(24, -24), - child: Align( - alignment: Alignment.bottomLeft, - child: ActionChip( - backgroundColor: Theme.of(context).colorScheme.primary, - side: BorderSide.none, - avatar: Icon( - Icons.expand_less, - color: Theme.of(context).colorScheme.onPrimary, - ), - onPressed: () async { - await pagingScrollController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInExpo, - ); - _nestedScrollViewKey.currentState?.outerController.jumpTo(0); - }, - label: Text( - S.of(context)!.scrollToTop, - style: DefaultTextStyle.of(context).style.apply( - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - ), - ), - ); - } - Widget _buildSavedViewsTab( ConnectivityState connectivityState, BuildContext context, ) { return RefreshIndicator( - edgeOffset: kToolbarHeight + kTextTabBarHeight, + edgeOffset: kTextTabBarHeight, onRefresh: _onReloadSavedViews, notificationPredicate: (_) => connectivityState.isConnected, child: CustomScrollView( key: const PageStorageKey("savedViews"), slivers: [ SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + handle: searchBarHandle, + ), + SliverOverlapInjector( + handle: tabBarHandle, ), const SavedViewList(), ], @@ -332,46 +288,75 @@ class _DocumentsPageState extends State BuildContext context, ) { return RefreshIndicator( - edgeOffset: kToolbarHeight + kTextTabBarHeight, + edgeOffset: kTextTabBarHeight, onRefresh: _onReloadDocuments, notificationPredicate: (_) => connectivityState.isConnected, - child: CustomScrollView( - key: const PageStorageKey("documents"), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - _buildViewActions(), - BlocBuilder( - builder: (context, state) { - if (state.hasLoaded && state.documents.isEmpty) { - return SliverToBoxAdapter( - child: DocumentsEmptyState( - state: state, - onReset: context.read().resetFilter, + child: NotificationListener( + onNotification: (notification) { + // Listen for scroll notifications to load new data. + // Scroll controller does not work here due to nestedscrollview limitations. + + final currState = context.read().state; + final max = notification.metrics.maxScrollExtent; + if (max == 0 || + _currentTab != 0 || + currState.isLoading || + currState.isLastPageLoaded) { + return true; + } + + final offset = notification.metrics.pixels; + if (offset >= max * 0.7) { + context + .read() + .loadMore() + .onError( + (error, stackTrace) => showErrorMessage( + context, + error, + stackTrace, ), ); - } + } + return false; + }, + child: CustomScrollView( + key: const PageStorageKey("documents"), + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + _buildViewActions(), + BlocBuilder( + builder: (context, state) { + if (state.hasLoaded && state.documents.isEmpty) { + return SliverToBoxAdapter( + child: DocumentsEmptyState( + state: state, + onReset: context.read().resetFilter, + ), + ); + } - return SliverAdaptiveDocumentsView( - viewType: state.viewType, - onTap: _openDetails, - onSelected: - context.read().toggleDocumentSelection, - hasInternetConnection: connectivityState.isConnected, - onTagSelected: _addTagToFilter, - onCorrespondentSelected: _addCorrespondentToFilter, - onDocumentTypeSelected: _addDocumentTypeToFilter, - onStoragePathSelected: _addStoragePathToFilter, - documents: state.documents, - hasLoaded: state.hasLoaded, - isLabelClickable: true, - isLoading: state.isLoading, - selectedDocumentIds: state.selectedIds, - ); - }, - ), - ], + return SliverAdaptiveDocumentsView( + viewType: state.viewType, + onTap: _openDetails, + onSelected: + context.read().toggleDocumentSelection, + hasInternetConnection: connectivityState.isConnected, + onTagSelected: _addTagToFilter, + onCorrespondentSelected: _addCorrespondentToFilter, + onDocumentTypeSelected: _addDocumentTypeToFilter, + onStoragePathSelected: _addStoragePathToFilter, + documents: state.documents, + hasLoaded: state.hasLoaded, + isLabelClickable: true, + isLoading: state.isLoading, + selectedDocumentIds: state.selectedIds, + ); + }, + ), + ], + ), ), ); } diff --git a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart new file mode 100644 index 0000000..4ee5265 --- /dev/null +++ b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; +import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:provider/provider.dart'; + +class DocumentSelectionSliverAppBar extends StatelessWidget { + final DocumentsState state; + const DocumentSelectionSliverAppBar({super.key, required this.state}); + + @override + Widget build(BuildContext context) { + return SliverAppBar( + pinned: true, + title: Text( + S.of(context)!.countSelected(state.selection.length), + ), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => context.read().resetSelection(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + final shouldDelete = await showDialog( + context: context, + builder: (context) => + BulkDeleteConfirmationDialog(state: state), + ) ?? + false; + if (shouldDelete) { + try { + await context + .read() + .bulkDelete(state.selection); + showSnackBar( + context, + S.of(context)!.documentsSuccessfullyDeleted, + ); + context.read().resetSelection(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + }, + ), + ], + ); + } +} diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 2f8f21a..7026e2b 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -8,6 +8,7 @@ import 'package:paperless_mobile/features/document_search/view/document_search_p import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/documents_list_loading_widget.dart'; import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.dart'; @@ -27,6 +28,9 @@ class InboxPage extends StatefulWidget { class _InboxPageState extends State with DocumentPagingViewMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); + @override final pagingScrollController = ScrollController(); final _emptyStateRefreshIndicatorKey = GlobalKey(); @@ -39,12 +43,6 @@ class _InboxPageState extends State @override Widget build(BuildContext context) { - final safeAreaPadding = MediaQuery.of(context).padding; - final availableHeight = MediaQuery.of(context).size.height - - kToolbarHeight - - kBottomNavigationBarHeight - - safeAreaPadding.top - - safeAreaPadding.bottom; return Scaffold( drawer: const AppDrawer(), floatingActionButton: BlocBuilder( @@ -68,99 +66,97 @@ class _InboxPageState extends State builder: (context, state) { return SafeArea( top: true, - child: Builder( - builder: (context) { - // Build a list of slivers alternating between SliverToBoxAdapter - // (group header) and a SliverList (inbox items). - final List slivers = _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), - ), - 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() - ..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); - // edgeOffset: kToolbarHeight, - - return RefreshIndicator( - edgeOffset: kToolbarHeight, - onRefresh: context.read().reload, - child: CustomScrollView( - physics: state.documents.isEmpty - ? const NeverScrollableScrollPhysics() - : const AlwaysScrollableScrollPhysics(), - controller: pagingScrollController, - slivers: [ - SearchAppBar( - hintText: S.of(context)!.searchDocuments, - onOpenSearch: showDocumentSearchPage, + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: const SliverSearchBar(), + ) + ], + body: Builder( + builder: (context) { + if (!state.hasLoaded) { + return const DocumentsListLoadingWidget(); //TODO: Implement InboxLoadingWidget... + } else if (state.documents.isEmpty) { + return Center( + child: InboxEmptyWidget( + emptyStateRefreshIndicatorKey: + _emptyStateRefreshIndicatorKey, ), - if (state.documents.isEmpty) - SliverToBoxAdapter( - child: SizedBox( - height: availableHeight, - child: Center( - child: InboxEmptyWidget( - emptyStateRefreshIndicatorKey: - _emptyStateRefreshIndicatorKey, - ), + ); + } else { + return RefreshIndicator( + edgeOffset: kToolbarHeight, + onRefresh: context.read().reload, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: HintCard( + show: !state.isHintAcknowledged, + hintText: + S.of(context)!.swipeLeftToMarkADocumentAsSeen, + onHintAcknowledged: () => + context.read().acknowledgeHint(), ), ), - ) - else if (!state.hasLoaded) - DocumentsListLoadingWidget() - else - 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), + ), + 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), ), - ), - ...slivers, - ], - ), - ); - }, + ], + ), + ); + } + }, + ), ), ); }, diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 7f5a26f..17a6de6 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; -import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; +import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; @@ -29,6 +30,11 @@ class LabelsPage extends StatefulWidget { class _LabelsPageState extends State with SingleTickerProviderStateMixin { + final SliverOverlapAbsorberHandle searchBarHandle = + SliverOverlapAbsorberHandle(); + final SliverOverlapAbsorberHandle tabBarHandle = + SliverOverlapAbsorberHandle(); + late final TabController _tabController; int _currentIndex = 0; @@ -46,217 +52,212 @@ class _LabelsPageState extends State length: 3, child: BlocBuilder( builder: (context, connectedState) { - return Scaffold( - drawer: const AppDrawer(), - floatingActionButton: FloatingActionButton( - onPressed: [ - _openAddCorrespondentPage, - _openAddDocumentTypePage, - _openAddTagPage, - _openAddStoragePathPage, - ][_currentIndex], - child: Icon(Icons.add), - ), - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - // This widget takes the overlapping behavior of the SliverAppBar, - // and redirects it to the SliverOverlapInjector below. If it is - // missing, then it is possible for the nested "inner" scroll view - // below to end up under the SliverAppBar even when the inner - // scroll view thinks it has not been scrolled. - // This is not necessary if the "headerSliverBuilder" only builds - // widgets that do not overlap the next sliver. - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, + return SafeArea( + child: Scaffold( + drawer: const AppDrawer(), + floatingActionButton: FloatingActionButton( + onPressed: [ + _openAddCorrespondentPage, + _openAddDocumentTypePage, + _openAddTagPage, + _openAddStoragePathPage, + ][_currentIndex], + child: const Icon(Icons.add), + ), + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: const SliverSearchBar(), ), - sliver: SearchAppBar( - hintText: S.of(context)!.searchDocuments, - onOpenSearch: showDocumentSearchPage, - bottom: TabBar( + SliverOverlapAbsorber( + handle: tabBarHandle, + sliver: SliverPersistentHeader( + pinned: true, + delegate: CustomizableSliverPersistentHeaderDelegate( + child: ColoredTabBar( + backgroundColor: + Theme.of(context).colorScheme.background, + tabBar: TabBar( + controller: _tabController, + tabs: [ + Tab( + icon: Icon( + Icons.person_outline, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + Tab( + icon: Icon( + Icons.description_outlined, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + Tab( + icon: Icon( + Icons.label_outline, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + Tab( + icon: Icon( + Icons.folder_open, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ], + ), + ), + minExtent: kTextTabBarHeight, + maxExtent: kTextTabBarHeight), + ), + ), + ], + body: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.maxScrollExtent == 0) { + return true; + } + final desiredTab = + ((metrics.pixels / metrics.maxScrollExtent) * + (_tabController.length - 1)) + .round(); + + if (metrics.axis == Axis.horizontal && + _currentIndex != desiredTab) { + setState(() => _currentIndex = desiredTab); + } + return true; + }, + child: RefreshIndicator( + edgeOffset: kToolbarHeight + kTextTabBarHeight, + notificationPredicate: (notification) => + connectedState.isConnected, + onRefresh: () => [ + context.read>(), + context.read>(), + context.read>(), + context.read>(), + ][_currentIndex] + .reload(), + child: TabBarView( controller: _tabController, - tabs: [ - Tab( - icon: Icon( - Icons.person_outline, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + children: [ + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + correspondent: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditCorrespondentPage, + emptyStateActionButtonLabel: + S.of(context)!.addNewCorrespondent, + emptyStateDescription: + S.of(context)!.noCorrespondentsSetUp, + onAddNew: _openAddCorrespondentPage, + ), + ], + ); + }, ), - Tab( - icon: Icon( - Icons.description_outlined, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + documentType: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditDocumentTypePage, + emptyStateActionButtonLabel: + S.of(context)!.addNewDocumentType, + emptyStateDescription: + S.of(context)!.noDocumentTypesSetUp, + onAddNew: _openAddDocumentTypePage, + ), + ], + ); + }, ), - Tab( - icon: Icon( - Icons.label_outline, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + filterBuilder: (label) => DocumentFilter( + tags: IdsTagsQuery.fromIds([label.id!]), + pageSize: label.documentCount ?? 0, + ), + onEdit: _openEditTagPage, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag ?? false + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + emptyStateActionButtonLabel: + S.of(context)!.addNewTag, + emptyStateDescription: + S.of(context)!.noTagsSetUp, + onAddNew: _openAddTagPage, + ), + ], + ); + }, ), - Tab( - icon: Icon( - Icons.folder_open, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), + Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + onEdit: _openEditStoragePathPage, + filterBuilder: (label) => DocumentFilter( + storagePath: + IdQueryParameter.fromId(label.id), + pageSize: label.documentCount ?? 0, + ), + contentBuilder: (path) => Text(path.path), + emptyStateActionButtonLabel: + S.of(context)!.addNewStoragePath, + emptyStateDescription: + S.of(context)!.noStoragePathsSetUp, + onAddNew: _openAddStoragePathPage, + ), + ], + ); + }, ), ], ), ), ), - ], - body: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.maxScrollExtent == 0) { - return true; - } - final desiredTab = - ((metrics.pixels / metrics.maxScrollExtent) * - (_tabController.length - 1)) - .round(); - - if (metrics.axis == Axis.horizontal && - _currentIndex != desiredTab) { - setState(() => _currentIndex = desiredTab); - } - return true; - }, - child: RefreshIndicator( - edgeOffset: kToolbarHeight + kTextTabBarHeight, - notificationPredicate: (notification) => - connectedState.isConnected, - onRefresh: () => [ - context.read>(), - context.read>(), - context.read>(), - context.read>(), - ][_currentIndex] - .reload(), - child: TabBarView( - controller: _tabController, - children: [ - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - LabelTabView( - filterBuilder: (label) => DocumentFilter( - correspondent: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: - S.of(context)!.addNewCorrespondent, - emptyStateDescription: - S.of(context)!.noCorrespondentsSetUp, - onAddNew: _openAddCorrespondentPage, - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - LabelTabView( - filterBuilder: (label) => DocumentFilter( - documentType: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: - S.of(context)!.addNewDocumentType, - emptyStateDescription: - S.of(context)!.noDocumentTypesSetUp, - onAddNew: _openAddDocumentTypePage, - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - LabelTabView( - filterBuilder: (label) => DocumentFilter( - tags: IdsTagsQuery.fromIds([label.id!]), - pageSize: label.documentCount ?? 0, - ), - onEdit: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag ?? false - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - emptyStateActionButtonLabel: - S.of(context)!.addNewTag, - emptyStateDescription: - S.of(context)!.noTagsSetUp, - onAddNew: _openAddTagPage, - ), - ], - ); - }, - ), - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView - .sliverOverlapAbsorberHandleFor(context), - ), - LabelTabView( - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => DocumentFilter( - storagePath: - IdQueryParameter.fromId(label.id), - pageSize: label.documentCount ?? 0, - ), - contentBuilder: (path) => Text(path.path), - emptyStateActionButtonLabel: - S.of(context)!.addNewStoragePath, - emptyStateDescription: - S.of(context)!.noStoragePathsSetUp, - onAddNew: _openAddStoragePathPage, - ), - ], - ); - }, - ), - ], - ), - ), ), ), ); diff --git a/lib/features/labels/view/widgets/label_text.dart b/lib/features/labels/view/widgets/label_text.dart index bf2e2f1..28a35e0 100644 --- a/lib/features/labels/view/widgets/label_text.dart +++ b/lib/features/labels/view/widgets/label_text.dart @@ -27,6 +27,8 @@ class LabelText extends StatelessWidget { return Text( state.labels[id]?.toString() ?? placeholder, style: style, + maxLines: 1, + overflow: TextOverflow.ellipsis, ); }, ), diff --git a/lib/l10n/intl_cs.arb b/lib/l10n/intl_cs.arb index 0fe34da..0a0f483 100644 --- a/lib/l10n/intl_cs.arb +++ b/lib/l10n/intl_cs.arb @@ -526,7 +526,7 @@ "@connectionSuccessfulylEstablished": {}, "hostCouldNotBeResolved": "Adresa nemohla být rozpoznána. Zkontrolujte prosím adresu serveru a své internetové připojení.", "@hostCouldNotBeResolved": {}, - "serverAddress": "'Adresa serveru", + "serverAddress": "Adresa serveru", "@serverAddress": {}, "invalidAddress": "Neplatná adresa", "@invalidAddress": {}, diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index a5cd2ee..5a9848a 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -672,8 +672,8 @@ "@grid": {}, "list": "Liste", "@list": {}, - "remove": "Remove", - "removeQueryFromSearchHistory": "Remove query from search history?", + "remove": "Entfernen", + "removeQueryFromSearchHistory": "Aus Suchverlauf entfernen?", "dynamicColorScheme": "Dynamisch", "@dynamicColorScheme": {}, "classicColorScheme": "Klassisch", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 9cd23e8..4713656 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,55 +1,55 @@ { - "developedBy": "Developed by {name}", + "developedBy": "Développé par {name}", "@developedBy": { "placeholders": { "name": {} } }, - "addAnotherAccount": "Add another account", + "addAnotherAccount": "Ajouter un autre compte", "@addAnotherAccount": {}, - "account": "Account", + "account": "Compte", "@account": {}, - "addCorrespondent": "New Correspondent", + "addCorrespondent": "Nouveau correspondant", "@addCorrespondent": { "description": "Title when adding a new correspondent" }, - "addDocumentType": "New Document Type", + "addDocumentType": "Nouveau type de document", "@addDocumentType": { "description": "Title when adding a new document type" }, - "addStoragePath": "New Storage Path", + "addStoragePath": "Nouveau chemin de stockage", "@addStoragePath": { "description": "Title when adding a new storage path" }, - "addTag": "New Tag", + "addTag": "Nouvelle étiquette", "@addTag": { "description": "Title when adding a new tag" }, - "aboutThisApp": "About this app", + "aboutThisApp": "À propos de cette application", "@aboutThisApp": { "description": "Label for about this app tile displayed in the drawer" }, - "loggedInAs": "Logged in as {name}", + "loggedInAs": "Connecté en tant que {name}", "@loggedInAs": { "placeholders": { "name": {} } }, - "disconnect": "Disconnect", + "disconnect": "Se déconnecter", "@disconnect": { "description": "Logout button label" }, - "reportABug": "Report a Bug", + "reportABug": "Signaler un bug", "@reportABug": {}, - "settings": "Settings", + "settings": "Paramètres", "@settings": {}, - "authenticateOnAppStart": "Authenticate on app start", + "authenticateOnAppStart": "S'authentifier au démarrage de l'application", "@authenticateOnAppStart": { "description": "Description of the biometric authentication settings tile" }, - "biometricAuthentication": "Biometric authentication", + "biometricAuthentication": "Authentification biométrique", "@biometricAuthentication": {}, - "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authenticate enable biometric authentication} disable{Authenticate to disable biometric authentication} other{}}", + "authenticateToToggleBiometricAuthentication": "{mode, select, enable{Authentifiez-vous pour activer l'authentification biométrique} disable{Authentifiez-vous pour désactiver l'authentification biométrique} other{}}", "@authenticateToToggleBiometricAuthentication": { "placeholders": { "mode": {} @@ -57,171 +57,171 @@ }, "documents": "Documents", "@documents": {}, - "inbox": "Inbox", + "inbox": "Boîte de réception", "@inbox": {}, - "labels": "Labels", + "labels": "Étiquettes", "@labels": {}, - "scanner": "Scanner", + "scanner": "Scanneur", "@scanner": {}, - "startTyping": "Start typing...", + "startTyping": "Commencez à écrire...", "@startTyping": {}, - "doYouReallyWantToDeleteThisView": "Do you really want to delete this view?", + "doYouReallyWantToDeleteThisView": "Voulez-vous vraiment supprimer cette vue enregistrée ?", "@doYouReallyWantToDeleteThisView": {}, - "deleteView": "Delete view ", + "deleteView": "Supprimer la vue enregistrée ", "@deleteView": {}, - "addedAt": "Added at", + "addedAt": "Date d’ajout", "@addedAt": {}, - "archiveSerialNumber": "Archive Serial Number", + "archiveSerialNumber": "Numéro de série de l’archive", "@archiveSerialNumber": {}, - "asn": "ASN", + "asn": "NSA", "@asn": {}, - "correspondent": "Correspondent", + "correspondent": "Correspondant", "@correspondent": {}, - "createdAt": "Created at", + "createdAt": "Date de création", "@createdAt": {}, - "documentSuccessfullyDeleted": "Document successfully deleted.", + "documentSuccessfullyDeleted": "Le document a bien été supprimé.", "@documentSuccessfullyDeleted": {}, - "assignAsn": "Assign ASN", + "assignAsn": "Assigner un NSA", "@assignAsn": {}, - "deleteDocumentTooltip": "Delete", + "deleteDocumentTooltip": "Supprimer", "@deleteDocumentTooltip": { "description": "Tooltip shown for the delete button on details page" }, - "downloadDocumentTooltip": "Download", + "downloadDocumentTooltip": "Télécharger", "@downloadDocumentTooltip": { "description": "Tooltip shown for the download button on details page" }, - "editDocumentTooltip": "Edit", + "editDocumentTooltip": "Modifier", "@editDocumentTooltip": { "description": "Tooltip shown for the edit button on details page" }, - "loadFullContent": "Load full content", + "loadFullContent": "Charger tout le contenu", "@loadFullContent": {}, - "noAppToDisplayPDFFilesFound": "No app to display PDF files found!", + "noAppToDisplayPDFFilesFound": "Aucune application trouvée pour afficher les fichiers PDF !", "@noAppToDisplayPDFFilesFound": {}, - "openInSystemViewer": "Open in system viewer", + "openInSystemViewer": "Ouvrir dans le lecteur système", "@openInSystemViewer": {}, - "couldNotOpenFilePermissionDenied": "Could not open file: Permission denied.", + "couldNotOpenFilePermissionDenied": "Impossible d'ouvrir le fichier : permission refusée.", "@couldNotOpenFilePermissionDenied": {}, - "previewTooltip": "Preview", + "previewTooltip": "Prévisualisation", "@previewTooltip": { "description": "Tooltip shown for the preview button on details page" }, - "shareTooltip": "Share", + "shareTooltip": "Partager", "@shareTooltip": { "description": "Tooltip shown for the share button on details page" }, - "similarDocuments": "Similar Documents", + "similarDocuments": "Documents similaires", "@similarDocuments": { "description": "Label shown in the tabbar on details page" }, - "content": "Content", + "content": "Contenu", "@content": { "description": "Label shown in the tabbar on details page" }, - "metaData": "Meta Data", + "metaData": "Métadonnées", "@metaData": { "description": "Label shown in the tabbar on details page" }, - "overview": "Overview", + "overview": "Vue d’ensemble", "@overview": { "description": "Label shown in the tabbar on details page" }, - "documentType": "Document Type", + "documentType": "Type de document", "@documentType": {}, - "archivedPdf": "Archived (pdf)", + "archivedPdf": "Archive (PDF)", "@archivedPdf": { "description": "Option to chose when downloading a document" }, - "chooseFiletype": "Choose filetype", + "chooseFiletype": "Choisissez le type de fichier", "@chooseFiletype": {}, "original": "Original", "@original": { "description": "Option to chose when downloading a document" }, - "documentSuccessfullyDownloaded": "Document successfully downloaded.", + "documentSuccessfullyDownloaded": "Document téléchargé avec succès.", "@documentSuccessfullyDownloaded": {}, - "suggestions": "Suggestions: ", + "suggestions": "Suggestions : ", "@suggestions": {}, - "editDocument": "Edit Document", + "editDocument": "Modifier le document", "@editDocument": {}, "advanced": "Advanced", "@advanced": {}, - "apply": "Apply", + "apply": "Appliquer", "@apply": {}, "extended": "Extended", "@extended": {}, - "titleAndContent": "Title & Content", + "titleAndContent": "Titre & contenu", "@titleAndContent": {}, - "title": "Title", + "title": "Titre", "@title": {}, - "reset": "Reset", + "reset": "Réinitialiser", "@reset": {}, - "filterDocuments": "Filter Documents", + "filterDocuments": "Filtrer les documents", "@filterDocuments": { "description": "Title of the document filter" }, - "originalMD5Checksum": "Original MD5-Checksum", + "originalMD5Checksum": "Somme de contrôle MD5 de l'original", "@originalMD5Checksum": {}, - "mediaFilename": "Media Filename", + "mediaFilename": "Nom de fichier du média", "@mediaFilename": {}, - "originalFileSize": "Original File Size", + "originalFileSize": "Taille de fichier de l'original", "@originalFileSize": {}, - "originalMIMEType": "Original MIME-Type", + "originalMIMEType": "Type mime de l'original", "@originalMIMEType": {}, - "modifiedAt": "Modified at", + "modifiedAt": "Date de modification", "@modifiedAt": {}, - "preview": "Preview", + "preview": "Prévisualisation", "@preview": { "description": "Title of the document preview page" }, - "scanADocument": "Scan a document", + "scanADocument": "Scanner un document", "@scanADocument": {}, - "noDocumentsScannedYet": "No documents scanned yet.", + "noDocumentsScannedYet": "Aucun document scanné pour le moment.", "@noDocumentsScannedYet": {}, - "or": "or", + "or": "ou", "@or": { "description": "Used on the scanner page between both main actions when no scans have been captured." }, - "deleteAllScans": "Delete all scans", + "deleteAllScans": "Supprimer tous les scans", "@deleteAllScans": {}, - "uploadADocumentFromThisDevice": "Upload a document from this device", + "uploadADocumentFromThisDevice": "Charger un document depuis cet appareil", "@uploadADocumentFromThisDevice": { "description": "Button label on scanner page" }, - "noMatchesFound": "No matches found.", + "noMatchesFound": "Aucune correspondance trouvée.", "@noMatchesFound": { "description": "Displayed when no documents were found in the document search." }, - "removeFromSearchHistory": "Remove from search history?", + "removeFromSearchHistory": "Supprimer de l'historique des recherches ?", "@removeFromSearchHistory": {}, - "results": "Results", + "results": "Résultats", "@results": { "description": "Label displayed above search results in document search." }, - "searchDocuments": "Search documents", + "searchDocuments": "Rechercher des documents", "@searchDocuments": {}, - "resetFilter": "Reset filter", + "resetFilter": "Réinitialiser le filtre", "@resetFilter": {}, - "lastMonth": "Last Month", + "lastMonth": "Le mois dernier", "@lastMonth": {}, - "last7Days": "Last 7 Days", + "last7Days": "Les 7 derniers jours", "@last7Days": {}, - "last3Months": "Last 3 Months", + "last3Months": "Les 3 derniers mois", "@last3Months": {}, - "lastYear": "Last Year", + "lastYear": "L'année dernière", "@lastYear": {}, - "search": "Search", + "search": "Recherche", "@search": {}, - "documentsSuccessfullyDeleted": "Documents successfully deleted.", + "documentsSuccessfullyDeleted": "Les documents ont bien été supprimés.", "@documentsSuccessfullyDeleted": {}, "thereSeemsToBeNothingHere": "There seems to be nothing here...", "@thereSeemsToBeNothingHere": {}, - "oops": "Oops.", + "oops": "Oups.", "@oops": {}, - "newDocumentAvailable": "New document available!", + "newDocumentAvailable": "Nouveau document disponible !", "@newDocumentAvailable": {}, - "orderBy": "Order By", + "orderBy": "Trier par", "@orderBy": {}, "thisActionIsIrreversibleDoYouWishToProceedAnyway": "This action is irreversible. Do you wish to proceed anyway?", "@thisActionIsIrreversibleDoYouWishToProceedAnyway": {}, @@ -244,17 +244,17 @@ "@storagePath": {}, "prepareDocument": "Prepare document", "@prepareDocument": {}, - "tags": "Tags", + "tags": "Étiquettes", "@tags": {}, - "documentSuccessfullyUpdated": "Document successfully updated.", + "documentSuccessfullyUpdated": "Le document a bien été modifié.", "@documentSuccessfullyUpdated": {}, - "fileName": "File Name", + "fileName": "Nom du fichier", "@fileName": {}, - "synchronizeTitleAndFilename": "Synchronize title and filename", + "synchronizeTitleAndFilename": "Synchroniser le titre et le nom du fichier", "@synchronizeTitleAndFilename": {}, - "reload": "Reload", + "reload": "Recharger", "@reload": {}, - "documentSuccessfullyUploadedProcessing": "Document successfully uploaded, processing...", + "documentSuccessfullyUploadedProcessing": "Le document a bien été chargé, traitement en cours...", "@documentSuccessfullyUploadedProcessing": {}, "deleteLabelWarningText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", "@deleteLabelWarningText": {}, @@ -282,7 +282,7 @@ "@youAreCurrentlyOffline": {}, "couldNotAssignArchiveSerialNumber": "Could not assign archive serial number.", "@couldNotAssignArchiveSerialNumber": {}, - "couldNotDeleteDocument": "Could not delete document, please try again.", + "couldNotDeleteDocument": "Impossible de supprimer le document, veuillez réessayer.", "@couldNotDeleteDocument": {}, "couldNotLoadDocuments": "Could not load documents, please try again.", "@couldNotLoadDocuments": {}, @@ -294,7 +294,7 @@ "@couldNotLoadDocumentTypes": {}, "couldNotUpdateDocument": "Could not update document, please try again.", "@couldNotUpdateDocument": {}, - "couldNotUploadDocument": "Could not upload document, please try again.", + "couldNotUploadDocument": "Impossible de charger le document, veuillez réessayer.", "@couldNotUploadDocument": {}, "invalidCertificateOrMissingPassphrase": "Invalid certificate or missing passphrase, please try again", "@invalidCertificateOrMissingPassphrase": {}, @@ -416,11 +416,11 @@ "@select": {}, "saveChanges": "Save changes", "@saveChanges": {}, - "upload": "Upload", + "upload": "Charger le document", "@upload": {}, "youreOffline": "You're offline.", "@youreOffline": {}, - "deleteDocument": "Delete document", + "deleteDocument": "Supprimer le document", "@deleteDocument": { "description": "Used as an action label on each inbox item" }, @@ -658,7 +658,7 @@ "@filterTags": {}, "inboxTag": "Inbox-Tag", "@inboxTag": {}, - "uploadInferValuesHint": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.", + "uploadInferValuesHint": "Si vous spécifiez des valeurs pour ces champs, votre instance Paperless ne dérivera pas automatiquement une valeur. Si vous voulez que ces valeurs soient automatiquement remplies par votre serveur, laissez les champs vides.", "@uploadInferValuesHint": {}, "useTheConfiguredBiometricFactorToAuthenticate": "Use the configured biometric factor to authenticate and unlock your documents.", "@useTheConfiguredBiometricFactorToAuthenticate": {}, diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index ef659ce..1120ad6 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -298,7 +298,7 @@ "@couldNotUploadDocument": {}, "invalidCertificateOrMissingPassphrase": "Invalid certificate or missing passphrase, please try again", "@invalidCertificateOrMissingPassphrase": {}, - "couldNotLoadSavedViews": "Could not load views.", + "couldNotLoadSavedViews": "Could not load saved views.", "@couldNotLoadSavedViews": {}, "aClientCertificateWasExpectedButNotSent": "A client certificate was expected but not sent. Please provide a valid client certificate.", "@aClientCertificateWasExpectedButNotSent": {}, @@ -488,7 +488,7 @@ "@noStoragePathsSetUp": {}, "storagePaths": "Storage Paths", "@storagePaths": {}, - "addNewTag": "Dodaj nowy tag", + "addNewTag": "Add new tag", "@addNewTag": {}, "noTagsSetUp": "You don't seem to have any tags set up.", "@noTagsSetUp": {},