mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 05:15:50 -06:00
FEATURE move button to the scanner page
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user