mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 08:07:56 -06:00
feat: Refactor export functionality, add new translations
This commit is contained in:
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user