feat: Refactor export functionality, add new translations

This commit is contained in:
Anton Stubenbord
2023-06-10 19:09:28 +02:00
parent b23bec4907
commit 46e30d833b
13 changed files with 348 additions and 226 deletions

View File

@@ -9,12 +9,19 @@ enum DialogConfirmButtonStyle {
class DialogConfirmButton<T> extends StatelessWidget { class DialogConfirmButton<T> extends StatelessWidget {
final DialogConfirmButtonStyle style; final DialogConfirmButtonStyle style;
final String? label; final String? label;
/// The value [Navigator.pop] will be called with. If [onPressed] is
/// specified, this value will be ignored.
final T? returnValue; final T? returnValue;
/// Function called when the button is pressed. Takes precedence over [returnValue].
final void Function()? onPressed;
const DialogConfirmButton({ const DialogConfirmButton({
super.key, super.key,
this.style = DialogConfirmButtonStyle.normal, this.style = DialogConfirmButtonStyle.normal,
this.label, this.label,
this.returnValue, this.returnValue,
this.onPressed,
}); });
@override @override
@@ -45,10 +52,13 @@ class DialogConfirmButton<T> extends StatelessWidget {
_style = _dangerStyle; _style = _dangerStyle;
break; break;
} }
final effectiveOnPressed =
onPressed ?? () => Navigator.of(context).pop(returnValue ?? true);
return ElevatedButton( return ElevatedButton(
child: Text(label ?? S.of(context)!.confirm), child: Text(label ?? S.of(context)!.confirm),
style: _style, style: _style,
onPressed: () => Navigator.of(context).pop(returnValue ?? true), onPressed: effectiveOnPressed,
); );
} }
} }

View File

@@ -41,8 +41,11 @@ class DocumentScannerCubit extends Cubit<List<File>> {
} }
} }
Future<void> saveLocally( Future<void> saveToFile(
Uint8List bytes, String fileName, String preferredLocaleSubtag) async { Uint8List bytes,
String fileName,
String preferredLocaleSubtag,
) async {
var file = await FileService.saveToFile(bytes, fileName); var file = await FileService.saveToFile(bytes, fileName);
_notificationService.notifyFileSaved( _notificationService.notifyFileSaved(
filename: fileName, filename: fileName,

View File

@@ -7,22 +7,19 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.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/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/service/file_description.dart'; import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/document_scan/view/widgets/export_scans_dialog.dart';
import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart'; import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
@@ -34,6 +31,7 @@ import 'package:path/path.dart' as p;
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:sliver_tools/sliver_tools.dart';
class ScannerPage extends StatefulWidget { class ScannerPage extends StatefulWidget {
const ScannerPage({Key? key}) : super(key: key); const ScannerPage({Key? key}) : super(key: key);
@@ -44,13 +42,12 @@ class ScannerPage extends StatefulWidget {
class _ScannerPageState extends State<ScannerPage> class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
static const fkFileName = "filename";
final SliverOverlapAbsorberHandle searchBarHandle = final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle(); SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle = final SliverOverlapAbsorberHandle actionsHandle =
SliverOverlapAbsorberHandle(); SliverOverlapAbsorberHandle();
final _downloadFormKey = GlobalKey<FormBuilderState>();
final _scrollController = ScrollController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -80,13 +77,8 @@ class _ScannerPageState extends State<ScannerPage>
), ),
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: actionsHandle, handle: actionsHandle,
sliver: SliverPersistentHeader( sliver: SliverPinnedHeader(
pinned: true, child: _buildActions(connectedState.isConnected),
delegate: CustomizableSliverPersistentHeaderDelegate(
child: _buildActions(connectedState.isConnected),
maxExtent: kTextTabBarHeight,
minExtent: kTextTabBarHeight,
),
), ),
), ),
], ],
@@ -121,151 +113,117 @@ class _ScannerPageState extends State<ScannerPage>
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: SizedBox( child: SizedBox(
height: kTextTabBarHeight, height: kTextTabBarHeight,
child: Row( child: BlocBuilder<DocumentScannerCubit, List<File>>(
crossAxisAlignment: CrossAxisAlignment.center, builder: (context, state) {
mainAxisAlignment: MainAxisAlignment.spaceAround, return RawScrollbar(
children: [ padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
BlocBuilder<DocumentScannerCubit, List<File>>( interactive: false,
builder: (context, state) { thumbVisibility: true,
return TextButton.icon( thickness: 2,
label: Text(S.of(context)!.previewScan), radius: Radius.circular(2),
style: TextButton.styleFrom( controller: _scrollController,
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10), child: ListView(
), controller: _scrollController,
onPressed: state.isNotEmpty scrollDirection: Axis.horizontal,
? () => Navigator.of(context).push( children: [
MaterialPageRoute( SizedBox(width: 12),
builder: (context) => DocumentView( TextButton.icon(
documentBytes: _assembleFileBytes( label: Text(S.of(context)!.previewScan),
state, style: TextButton.styleFrom(
forcePdf: true, padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
).then((file) => file.bytes), ),
onPressed: state.isNotEmpty
? () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes: _assembleFileBytes(
state,
forcePdf: true,
).then((file) => file.bytes),
),
), ),
), )
) : null,
: null, icon: const Icon(Icons.visibility_outlined),
icon: const Icon(Icons.visibility_outlined),
);
},
),
BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
return TextButton.icon(
label: Text(S.of(context)!.export),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
), ),
onPressed: state.isEmpty SizedBox(width: 8),
? null TextButton.icon(
: () { label: Text(S.of(context)!.clearAll),
showDialog( style: TextButton.styleFrom(
context: context, padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
builder: (BuildContext context) { ),
return AlertDialog( onPressed: state.isEmpty ? null : () => _reset(context),
title: Text(S.of(context)!.export), icon: const Icon(Icons.delete_sweep_outlined),
content: FormBuilder(
key: _downloadFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
autovalidateMode:
AutovalidateMode.always,
validator: (value) {
if (value?.trim().isEmpty ??
true) {
return S
.of(context)!
.thisFieldIsRequired;
}
if (value?.trim().contains(
RegExp(
r'[<>:"/|?*]')) ??
true) {
return S
.of(context)!
.invalidFilenameCharacter;
}
return null;
},
decoration: InputDecoration(
labelText:
S.of(context)!.fileName,
),
name: fkFileName,
)),
],
),
),
actions: [
const DialogCancelButton(),
ElevatedButton(
child: Text(S.of(context)!.export),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(
Theme.of(context)
.colorScheme
.primaryContainer,
),
foregroundColor:
MaterialStatePropertyAll(
Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
onPressed: () => {
if (_downloadFormKey.currentState!
.validate())
{
_onLocalSave().then((value) =>
Navigator.of(context).pop())
}
},
),
],
);
});
},
icon: const Icon(Icons.download_outlined),
);
},
),
BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
return TextButton.icon(
label: Text(S.of(context)!.clearAll),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
), ),
onPressed: state.isEmpty ? null : () => _reset(context), SizedBox(width: 8),
icon: const Icon(Icons.delete_sweep_outlined), TextButton.icon(
); label: Text(S.of(context)!.upload),
}, style: TextButton.styleFrom(
), padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
BlocBuilder<DocumentScannerCubit, List<File>>( ),
builder: (context, state) { onPressed: state.isEmpty || !isConnected
return TextButton.icon( ? null
label: Text(S.of(context)!.upload), : () => _onPrepareDocumentUpload(context),
style: TextButton.styleFrom( icon: const Icon(Icons.upload_outlined),
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
), ),
onPressed: state.isEmpty || !isConnected SizedBox(width: 8),
? null TextButton.icon(
: () => _onPrepareDocumentUpload(context), label: Text(S.of(context)!.export),
icon: const Icon(Icons.upload_outlined), style: TextButton.styleFrom(
); padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
}, ),
), onPressed: state.isEmpty ? null : _onSaveToFile,
], icon: const Icon(Icons.save_alt_outlined),
),
SizedBox(width: 12),
],
),
);
},
), ),
), ),
); );
} }
void _onSaveToFile() async {
final fileName = await showDialog<String>(
context: context,
builder: (context) => const ExportScansDialog(),
);
if (fileName != null) {
final cubit = context.read<DocumentScannerCubit>();
final file = await _assembleFileBytes(
forcePdf: true,
context.read<DocumentScannerCubit>().state,
);
try {
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (Platform.isAndroid && androidInfo!.version.sdkInt <= 29) {
final isGranted = await askForPermission(Permission.storage);
if (!isGranted) {
showSnackBar(
context,
"Please grant permissions for Paperless Mobile to access your filesystem.",
action: SnackBarActionConfig(
label: "GO",
onPressed: openAppSettings,
),
);
return;
}
}
await cubit.saveToFile(
file.bytes,
"$fileName.pdf",
globalSettings.preferredLocaleSubtag,
);
} catch (error) {
showGenericError(context, error);
}
}
}
void _openDocumentScanner(BuildContext context) async { void _openDocumentScanner(BuildContext context) async {
final isGranted = await askForPermission(Permission.camera); final isGranted = await askForPermission(Permission.camera);
if (!isGranted) { if (!isGranted) {
@@ -311,34 +269,6 @@ class _ScannerPageState extends State<ScannerPage>
} }
} }
Future<void> _onLocalSave() async {
final cubit = context.read<DocumentScannerCubit>();
final file = await _assembleFileBytes(
forcePdf: true,
context.read<DocumentScannerCubit>().state,
);
try {
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (Platform.isAndroid && androidInfo!.version.sdkInt <= 29) {
final isGranted = await askForPermission(Permission.storage);
if (!isGranted) {
return;
//TODO: Ask user to grant permissions
}
}
final name =
_downloadFormKey.currentState?.fields[fkFileName]!.value as String;
var fileName = "$name.pdf";
await cubit.saveLocally(
file.bytes, fileName, globalSettings.preferredLocaleSubtag);
} catch (error) {
showGenericError(context, error);
}
}
Widget _buildEmptyState(bool isConnected, List<File> scans) { Widget _buildEmptyState(bool isConnected, List<File> scans) {
if (scans.isNotEmpty) { if (scans.isNotEmpty) {
return _buildImageGrid(scans); return _buildImageGrid(scans);
@@ -369,34 +299,37 @@ class _ScannerPageState extends State<ScannerPage>
} }
Widget _buildImageGrid(List<File> scans) { Widget _buildImageGrid(List<File> scans) {
return CustomScrollView( return Padding(
slivers: [ padding: const EdgeInsets.all(8.0),
SliverOverlapInjector(handle: searchBarHandle), child: CustomScrollView(
SliverOverlapInjector(handle: actionsHandle), slivers: [
SliverGrid.builder( SliverOverlapInjector(handle: searchBarHandle),
itemCount: scans.length, SliverOverlapInjector(handle: actionsHandle),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( SliverGrid.builder(
crossAxisCount: 3, itemCount: scans.length,
childAspectRatio: 1 / sqrt(2), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 10, crossAxisCount: 3,
mainAxisSpacing: 10, childAspectRatio: 1 / sqrt(2),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (context, index) {
return ScannedImageItem(
file: scans[index],
onDelete: () async {
try {
context.read<DocumentScannerCubit>().removeScan(index);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
index: index,
totalNumberOfFiles: scans.length,
);
},
), ),
itemBuilder: (context, index) { ],
return ScannedImageItem( ),
file: scans[index],
onDelete: () async {
try {
context.read<DocumentScannerCubit>().removeScan(index);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
index: index,
totalNumberOfFiles: scans.length,
);
},
),
],
); );
} }

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class ExportScansDialog extends StatefulWidget {
const ExportScansDialog({super.key});
@override
State<ExportScansDialog> createState() => _ExportScansDialogState();
}
class _ExportScansDialogState extends State<ExportScansDialog> {
final _formKey = GlobalKey<FormState>();
String? _filename;
late String _placeholder;
@override
void initState() {
super.initState();
final date = DateFormat("yyyy_MM_ddThhmmss").format(DateTime.now());
_placeholder = "paperless_mobile_scan_$date";
}
@override
Widget build(BuildContext context) {
return AlertDialog(
insetPadding: EdgeInsets.all(8),
title: Text(S.of(context)!.exportScansToPdf),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context)!.allScansWillBeMerged),
SizedBox(height: 16),
TextFormField(
onSaved: (newValue) {
_filename = newValue;
},
autofocus: true,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
final matches = RegExp(r'[<>:"/\|?*]').allMatches(value!);
if (matches.isNotEmpty) {
final illegalCharacters = matches
.map((match) => match.group(0))
.toList()
.toSet()
.join(" ");
return S
.of(context)!
.invalidFilenameCharacter(illegalCharacters);
}
return null;
},
decoration: InputDecoration(
labelText: S.of(context)!.fileName,
errorMaxLines: 5,
suffixText: ".pdf",
hintText: _placeholder,
),
),
],
),
),
actions: [
const DialogCancelButton(),
DialogConfirmButton(
label: S.of(context)!.export,
onPressed: () async {
if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState?.save();
final effectiveFilename = (_filename?.trim().isEmpty ?? true)
? _placeholder
: _filename;
Navigator.pop(context, effectiveFilename);
}
},
),
],
);
}
}

View File

@@ -61,7 +61,7 @@ class _ScannedImageItemState extends State<ScannedImageItem> {
width: double.infinity, width: double.infinity,
height: 100, height: 100,
child: FittedBox( child: FittedBox(
fit: BoxFit.fill, fit: BoxFit.cover,
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.antiAliasWithSaveLayer,
alignment: Alignment.center, alignment: Alignment.center,
child: Image.file( child: Image.file(

View File

@@ -811,5 +811,18 @@
"goToLogin": "Anar al login", "goToLogin": "Anar al login",
"@goToLogin": { "@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
} },
"export": "Exporta",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }

View File

@@ -811,5 +811,18 @@
"goToLogin": "Go to login", "goToLogin": "Go to login",
"@goToLogin": { "@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
} },
"export": "Export",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }

View File

@@ -811,5 +811,18 @@
"goToLogin": "Gehe zur Anmeldung", "goToLogin": "Gehe zur Anmeldung",
"@goToLogin": { "@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
} },
"export": "Exportieren",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Ungültige(s) Zeichen im Dateinamen gefunden: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Scans als PDF exportieren",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "Alle Scans werden in eine einzige PDF-Datei zusammengeführt."
} }

View File

@@ -816,8 +816,13 @@
"@export": { "@export": {
"description": "Label for button that exports scanned images to pdf (before upload)" "description": "Label for button that exports scanned images to pdf (before upload)"
}, },
"invalidFilenameCharacter": "Invalid filename character found", "invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}",
"@invalidFilenameCharacter": { "@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue" "description": "For validating filename in export dialogue"
} },
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }

View File

@@ -780,36 +780,49 @@
"@alwaysAsk": { "@alwaysAsk": {
"description": "Option to choose when the app should always ask the user which filetype to use" "description": "Option to choose when the app should always ask the user which filetype to use"
}, },
"disableMatching": "Do not tag documents automatically", "disableMatching": "Ne pas étiqueter les documents automatiquement",
"@disableMatching": { "@disableMatching": {
"description": "One of the options for automatic tagging of documents" "description": "One of the options for automatic tagging of documents"
}, },
"none": "None", "none": "Aucun",
"@none": { "@none": {
"description": "One of available enum values of matching algorithm for tags" "description": "One of available enum values of matching algorithm for tags"
}, },
"logInToExistingAccount": "Log in to existing account", "logInToExistingAccount": "Se connecter à un compte existant",
"@logInToExistingAccount": { "@logInToExistingAccount": {
"description": "Title shown on login page if at least one user is already known to the app." "description": "Title shown on login page if at least one user is already known to the app."
}, },
"print": "Print", "print": "Imprimer",
"@print": { "@print": {
"description": "Tooltip for print button" "description": "Tooltip for print button"
}, },
"managePermissions": "Manage permissions", "managePermissions": "Gérer les permissions",
"@managePermissions": { "@managePermissions": {
"description": "Button which leads user to manage permissions page" "description": "Button which leads user to manage permissions page"
}, },
"errorRetrievingServerVersion": "An error occurred trying to resolve the server version.", "errorRetrievingServerVersion": "Une erreur est survenue en essayant de résoudre la version du serveur.",
"@errorRetrievingServerVersion": { "@errorRetrievingServerVersion": {
"description": "Message shown at the bottom of the settings page when the remote server version could not be resolved." "description": "Message shown at the bottom of the settings page when the remote server version could not be resolved."
}, },
"resolvingServerVersion": "Resolving server version...", "resolvingServerVersion": "Résolution de la version du serveur...",
"@resolvingServerVersion": { "@resolvingServerVersion": {
"description": "Message shown while the app is loading the remote server version." "description": "Message shown while the app is loading the remote server version."
}, },
"goToLogin": "Go to login", "goToLogin": "Se connecter",
"@goToLogin": { "@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
} },
"export": "Export",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }

View File

@@ -819,5 +819,10 @@
"invalidFilenameCharacter": "Znaleziono niedozwolony znak w nazwie pliku", "invalidFilenameCharacter": "Znaleziono niedozwolony znak w nazwie pliku",
"@invalidFilenameCharacter": { "@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue" "description": "For validating filename in export dialogue"
} },
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }

View File

@@ -811,5 +811,18 @@
"goToLogin": "Go to login", "goToLogin": "Go to login",
"@goToLogin": { "@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
} },
"export": "Export",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }

View File

@@ -811,5 +811,18 @@
"goToLogin": "Go to login", "goToLogin": "Go to login",
"@goToLogin": { "@goToLogin": {
"description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page" "description": "Label of the button shown on the login page to skip logging in to existing accounts and navigate user to login page"
} },
"export": "Export",
"@export": {
"description": "Label for button that exports scanned images to pdf (before upload)"
},
"invalidFilenameCharacter": "Invalid character(s) found in filename: {characters}",
"@invalidFilenameCharacter": {
"description": "For validating filename in export dialogue"
},
"exportScansToPdf": "Export scans to PDF",
"@exportScansToPdf": {
"description": "title of the alert dialog when exporting scans to pdf"
},
"allScansWillBeMerged": "All scans will be merged into a single PDF file."
} }