FEATURE move button to the scanner page

This commit is contained in:
konrad.lys@eu.equinix.com
2023-06-05 20:07:38 +02:00
parent dc552dc4a7
commit bf0351f23f
6 changed files with 142 additions and 82 deletions

View File

@@ -347,7 +347,6 @@ Future<DocumentUploadResult?> pushDocumentUploadPreparationPage(
create: (_) => DocumentUploadCubit( create: (_) => DocumentUploadCubit(
context.read(), context.read(),
context.read(), context.read(),
context.read(),
), ),
child: DocumentUploadPreparationPage( child: DocumentUploadPreparationPage(
fileBytes: bytes, fileBytes: bytes,

View File

@@ -5,9 +5,13 @@ 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:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
class DocumentScannerCubit extends Cubit<List<File>> { class DocumentScannerCubit extends Cubit<List<File>> {
DocumentScannerCubit() : super(const []); final LocalNotificationService _notificationService;
DocumentScannerCubit(this._notificationService) : super(const []);
void addScan(File file) => emit([...state, file]); void addScan(File file) => emit([...state, file]);
@@ -36,4 +40,15 @@ class DocumentScannerCubit extends Cubit<List<File>> {
throw const PaperlessServerException(ErrorCode.scanRemoveFailed); throw const PaperlessServerException(ErrorCode.scanRemoveFailed);
} }
} }
Future<void> saveLocally(
Uint8List bytes, String fileName, String preferredLocaleSubtag) async {
var file = await FileService.saveToFile(bytes, fileName);
_notificationService.notifyFileSaved(
filename: fileName,
filePath: file.path,
finished: true,
locale: preferredLocaleSubtag,
);
}
} }

View File

@@ -7,8 +7,13 @@ 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:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.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/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.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';
@@ -35,9 +40,13 @@ class ScannerPage extends StatefulWidget {
State<ScannerPage> createState() => _ScannerPageState(); State<ScannerPage> createState() => _ScannerPageState();
} }
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin { class _ScannerPageState extends State<ScannerPage>
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle(); with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle(); final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle =
SliverOverlapAbsorberHandle();
final _downloadFormKey = GlobalKey<FormBuilderState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -116,11 +125,13 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
builder: (context, state) { builder: (context, state) {
return TextButton.icon( return TextButton.icon(
label: Text(S.of(context)!.previewScan), label: Text(S.of(context)!.previewScan),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isNotEmpty onPressed: state.isNotEmpty
? () => Navigator.of(context).push( ? () => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => DocumentView( builder: (context) => DocumentView(
documentBytes: _assembleFileBytes( documentBytes: _assembleFileBytes(
state, state,
forcePdf: true, forcePdf: true,
@@ -133,10 +144,82 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
); );
}, },
), ),
BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
return TextButton.icon(
label: Text("Export"),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isEmpty
? null
: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Stack(
children: [
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 == null ||
value.isEmpty) {
return 'Please enter some text';
}
return null;
},
decoration: InputDecoration(
labelText:
S.of(context)!.fileName,
),
initialValue: "test",
name: 'filename',
)),
TextButton.icon(
label: const Text(
"Save a local copy"),
icon: const Icon(Icons.download),
onPressed: () => {
if (_downloadFormKey
.currentState!
.validate())
{
_onLocalSave().then(
(value) => Navigator.of(
context)
.pop())
}
},
)
],
),
),
],
),
);
});
},
icon: const Icon(Icons.download),
);
},
),
BlocBuilder<DocumentScannerCubit, List<File>>( BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) { builder: (context, state) {
return TextButton.icon( return TextButton.icon(
label: Text(S.of(context)!.clearAll), label: Text(S.of(context)!.clearAll),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isEmpty ? null : () => _reset(context), onPressed: state.isEmpty ? null : () => _reset(context),
icon: const Icon(Icons.delete_sweep_outlined), icon: const Icon(Icons.delete_sweep_outlined),
); );
@@ -146,6 +229,9 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
builder: (context, state) { builder: (context, state) {
return TextButton.icon( return TextButton.icon(
label: Text(S.of(context)!.upload), label: Text(S.of(context)!.upload),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isEmpty || !isConnected onPressed: state.isEmpty || !isConnected
? null ? null
: () => _onPrepareDocumentUpload(context), : () => _onPrepareDocumentUpload(context),
@@ -175,7 +261,8 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
final success = await EdgeDetection.detectEdge(file.path); final success = await EdgeDetection.detectEdge(file.path);
if (!success) { if (!success) {
if (kDebugMode) { if (kDebugMode) {
dev.log('[ScannerPage] Scan either not successful or canceled by user.'); dev.log(
'[ScannerPage] Scan either not successful or canceled by user.');
} }
return; return;
} }
@@ -197,7 +284,38 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) { if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
// For paperless version older than 1.11.3, task id will always be null! // For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset(); context.read<DocumentScannerCubit>().reset();
context.read<TaskStatusCubit>().listenToTaskChanges(uploadResult!.taskId!); context
.read<TaskStatusCubit>()
.listenToTaskChanges(uploadResult!.taskId!);
}
}
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['filename']?.value ??
"") as String;
var fileName = "$name.pdf";
await cubit.saveLocally(
file.bytes, fileName, globalSettings.preferredLocaleSubtag);
_downloadFormKey.currentState!.save();
} catch (error) {
showGenericError(context, error);
} }
} }

View File

@@ -5,8 +5,6 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
part 'document_upload_state.dart'; part 'document_upload_state.dart';
@@ -15,9 +13,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final LabelRepository _labelRepository; final LabelRepository _labelRepository;
final LocalNotificationService _notificationService; DocumentUploadCubit(this._labelRepository, this._documentApi)
DocumentUploadCubit(this._labelRepository, this._documentApi, this._notificationService)
: super(const DocumentUploadState()) { : super(const DocumentUploadState()) {
_labelRepository.addListener( _labelRepository.addListener(
this, this,
@@ -53,18 +49,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
); );
} }
Future<void> saveLocally(
Uint8List bytes, String fileName, String preferredLocaleSubtag
) async {
var file = await FileService.saveToFile(bytes, fileName);
_notificationService.notifyFileSaved(
filename: fileName,
filePath: file.path,
finished: true,
locale: preferredLocaleSubtag,
);
}
@override @override
Future<void> close() async { Future<void> close() async {
_labelRepository.removeListener(this); _labelRepository.removeListener(this);

View File

@@ -1,17 +1,12 @@
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
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:flutter_form_builder/flutter_form_builder.dart';
import 'package:hive/hive.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.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/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
@@ -24,8 +19,6 @@ import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.d
import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/helpers/permission_helpers.dart';
import 'package:permission_handler/permission_handler.dart';
class DocumentUploadResult { class DocumentUploadResult {
final bool success; final bool success;
@@ -82,8 +75,6 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0)) child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0))
: null, : null,
), ),
bottomNavigationBar: _buildBottomAppBar(),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0, visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended( child: FloatingActionButton.extended(
@@ -300,30 +291,6 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
} }
} }
BlocBuilder<DocumentUploadCubit, DocumentUploadState> _buildBottomAppBar() {
return BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, state) {
return BottomAppBar(
child: BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, connectivityState) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
tooltip: "Save a local copy",
icon: const Icon(Icons.download),
onPressed: () => _onLocalSave(),
).paddedOnly(right: 4.0),
],
);
},
),
);
},
);
}
String _padWithExtension(String source, [String? extension]) { String _padWithExtension(String source, [String? extension]) {
final ext = extension ?? '.pdf'; final ext = extension ?? '.pdf';
return source.endsWith(ext) ? source : '$source$ext'; return source.endsWith(ext) ? source : '$source$ext';
@@ -332,27 +299,4 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
String _formatFilename(String source) { String _formatFilename(String source) {
return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase(); return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase();
} }
Future<void> _onLocalSave() async {
final cubit = context.read<DocumentUploadCubit>();
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 title = (_formKey.currentState?.fields[fkFileName]?.value ?? widget.filename) as String;
var fileName = "$title.${widget.fileExtension}";
await cubit.saveLocally(widget.fileBytes, fileName, globalSettings.preferredLocaleSubtag);
} catch (error) {
showGenericError(context, error);
}
}
} }

View File

@@ -117,7 +117,7 @@ class HomeRoute extends StatelessWidget {
.get(currentLocalUserId)!, .get(currentLocalUserId)!,
)..reload(), )..reload(),
), ),
Provider(create: (context) => DocumentScannerCubit()), Provider(create: (context) => DocumentScannerCubit(context.read())),
ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository, ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository,
DocumentChangedNotifier, InboxCubit>( DocumentChangedNotifier, InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier, previous) => update: (context, docApi, statsApi, labelRepo, notifier, previous) =>