mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 13:15:49 -06:00
feat: Update translations, fix scrolling on all pages
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -63,4 +63,6 @@ untranslated_messages.txt
|
||||
#lakos generated files
|
||||
**/dot_images/*
|
||||
|
||||
docker/
|
||||
docker/
|
||||
|
||||
crowdin_credentials.yml
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ScannerPage>
|
||||
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<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectedState) {
|
||||
return Scaffold(
|
||||
@@ -68,54 +66,49 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
// ),
|
||||
body: BlocBuilder<DocumentScannerCubit, List<File>>(
|
||||
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<DocumentScannerCubit, List<File>>(
|
||||
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<ScannerPage>
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(bool isConnected) {
|
||||
return BlocBuilder<DocumentScannerCubit, List<File>>(
|
||||
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<File> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
59
lib/features/document_search/view/sliver_search_bar.dart
Normal file
59
lib/features/document_search/view/sliver_search_bar.dart
Normal file
@@ -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<PaperlessServerInformationCubit,
|
||||
PaperlessServerInformationState>(
|
||||
builder: (context, state) {
|
||||
return CircleAvatar(
|
||||
child: Text(state.information?.userInitials ?? ''),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const AccountSettingsDialog(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<DocumentsPage>
|
||||
with
|
||||
SingleTickerProviderStateMixin,
|
||||
DocumentPagingViewMixin<DocumentsPage, DocumentsCubit> {
|
||||
with SingleTickerProviderStateMixin {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
final SliverOverlapAbsorberHandle tabBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
ScrollController get pagingScrollController =>
|
||||
_nestedScrollViewKey.currentState?.innerController ?? ScrollController();
|
||||
|
||||
final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey();
|
||||
int _currentTab = 0;
|
||||
bool _showBackToTopButton = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -73,32 +69,13 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
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<DocumentsPage>
|
||||
}
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
return Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
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<DocumentsCubit, DocumentsState>(
|
||||
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<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
NestedScrollView(
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
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<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().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<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isNotEmpty) {
|
||||
return SliverAppBar(
|
||||
floating: false,
|
||||
SliverOverlapAbsorber(
|
||||
handle: tabBarHandle,
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
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<DocumentsCubit>()
|
||||
.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<ScrollNotification>(
|
||||
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<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return _buildSavedViewsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
body: NotificationListener<ScrollNotification>(
|
||||
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<DocumentsPage>
|
||||
);
|
||||
}
|
||||
|
||||
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<String>("savedViews"),
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
handle: searchBarHandle,
|
||||
),
|
||||
SliverOverlapInjector(
|
||||
handle: tabBarHandle,
|
||||
),
|
||||
const SavedViewList(),
|
||||
],
|
||||
@@ -332,46 +288,75 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
BuildContext context,
|
||||
) {
|
||||
return RefreshIndicator(
|
||||
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||
edgeOffset: kTextTabBarHeight,
|
||||
onRefresh: _onReloadDocuments,
|
||||
notificationPredicate: (_) => connectivityState.isConnected,
|
||||
child: CustomScrollView(
|
||||
key: const PageStorageKey<String>("documents"),
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
_buildViewActions(),
|
||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.hasLoaded && state.documents.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: context.read<DocumentsCubit>().resetFilter,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
// Listen for scroll notifications to load new data.
|
||||
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||
|
||||
final currState = context.read<DocumentsCubit>().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<DocumentsCubit>()
|
||||
.loadMore()
|
||||
.onError<PaperlessServerException>(
|
||||
(error, stackTrace) => showErrorMessage(
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
key: const PageStorageKey<String>("documents"),
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(handle: searchBarHandle),
|
||||
SliverOverlapInjector(handle: tabBarHandle),
|
||||
_buildViewActions(),
|
||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.hasLoaded && state.documents.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: context.read<DocumentsCubit>().resetFilter,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverAdaptiveDocumentsView(
|
||||
viewType: state.viewType,
|
||||
onTap: _openDetails,
|
||||
onSelected:
|
||||
context.read<DocumentsCubit>().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<DocumentsCubit>().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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<DocumentsCubit>().resetSelection(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
BulkDeleteConfirmationDialog(state: state),
|
||||
) ??
|
||||
false;
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
await context
|
||||
.read<DocumentsCubit>()
|
||||
.bulkDelete(state.selection);
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context)!.documentsSuccessfullyDeleted,
|
||||
);
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<InboxPage>
|
||||
with DocumentPagingViewMixin<InboxPage, InboxCubit> {
|
||||
final SliverOverlapAbsorberHandle searchBarHandle =
|
||||
SliverOverlapAbsorberHandle();
|
||||
|
||||
@override
|
||||
final pagingScrollController = ScrollController();
|
||||
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||
@@ -39,12 +43,6 @@ class _InboxPageState extends State<InboxPage>
|
||||
|
||||
@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<InboxCubit, InboxState>(
|
||||
@@ -68,99 +66,97 @@ class _InboxPageState extends State<InboxPage>
|
||||
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<Widget> 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<InboxCubit>().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<InboxCubit>().reload,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: HintCard(
|
||||
show: !state.isHintAcknowledged,
|
||||
hintText:
|
||||
S.of(context)!.swipeLeftToMarkADocumentAsSeen,
|
||||
onHintAcknowledged: () =>
|
||||
context.read<InboxCubit>().acknowledgeHint(),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (!state.hasLoaded)
|
||||
DocumentsListLoadingWidget()
|
||||
else
|
||||
SliverToBoxAdapter(
|
||||
child: HintCard(
|
||||
show: !state.isHintAcknowledged,
|
||||
hintText:
|
||||
S.of(context)!.swipeLeftToMarkADocumentAsSeen,
|
||||
onHintAcknowledged: () =>
|
||||
context.read<InboxCubit>().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,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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<LabelsPage>
|
||||
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<LabelsPage>
|
||||
length: 3,
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
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<ScrollNotification>(
|
||||
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<LabelCubit<Correspondent>>(),
|
||||
context.read<LabelCubit<DocumentType>>(),
|
||||
context.read<LabelCubit<Tag>>(),
|
||||
context.read<LabelCubit<StoragePath>>(),
|
||||
][_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<Correspondent>(
|
||||
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<DocumentType>(
|
||||
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<Tag>(
|
||||
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<StoragePath>(
|
||||
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<ScrollNotification>(
|
||||
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<LabelCubit<Correspondent>>(),
|
||||
context.read<LabelCubit<DocumentType>>(),
|
||||
context.read<LabelCubit<Tag>>(),
|
||||
context.read<LabelCubit<StoragePath>>(),
|
||||
][_currentIndex]
|
||||
.reload(),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
LabelTabView<Correspondent>(
|
||||
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<DocumentType>(
|
||||
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<Tag>(
|
||||
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<StoragePath>(
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -27,6 +27,8 @@ class LabelText<T extends Label> extends StatelessWidget {
|
||||
return Text(
|
||||
state.labels[id]?.toString() ?? placeholder,
|
||||
style: style,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
Reference in New Issue
Block a user