feat: Implement updated receive share logic

This commit is contained in:
Anton Stubenbord
2023-10-02 23:59:42 +02:00
parent 653344c9ee
commit 37ed8bbb04
47 changed files with 1695 additions and 730 deletions
@@ -0,0 +1,110 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
import 'package:paperless_mobile/features/sharing/view/widgets/upload_queue_shell.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';
class ConsumptionQueueView extends StatelessWidget {
const ConsumptionQueueView({super.key});
@override
Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>();
return Scaffold(
appBar: AppBar(
title: Text("Upload Queue"), //TODO: INTL
),
body: Consumer<ConsumptionChangeNotifier>(
builder: (context, value, child) {
if (value.pendingFiles.isEmpty) {
return Center(
child: Text("No pending files."),
);
}
return ListView.builder(
itemBuilder: (context, index) {
final file = value.pendingFiles.elementAt(index);
final filename = p.basename(file.path);
return ListTile(
title: Text(filename),
leading: Padding(
padding: const EdgeInsets.all(4),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FileThumbnail(
file: file,
fit: BoxFit.cover,
width: 75,
),
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
context
.read<ConsumptionChangeNotifier>()
.discardFile(file, userId: currentUser.id);
},
),
);
return Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
children: [
Text(filename, maxLines: 1),
SizedBox(
height: 56,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
ActionChip(
label: Text(S.of(context)!.upload),
avatar: Icon(Icons.file_upload_outlined),
onPressed: () {
consumeLocalFile(
context,
file: file,
userId: currentUser.id,
);
},
),
SizedBox(width: 8),
ActionChip(
label: Text(S.of(context)!.discard),
avatar: Icon(Icons.delete),
onPressed: () {
context
.read<ConsumptionChangeNotifier>()
.discardFile(
file,
userId: currentUser.id,
);
},
),
],
),
),
],
).padded(),
),
],
).padded();
},
itemCount: value.pendingFiles.length,
);
},
),
);
}
}
@@ -0,0 +1,51 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.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/core/widgets/future_or_builder.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:transparent_image/transparent_image.dart';
class DiscardSharedFileDialog extends StatelessWidget {
final FutureOr<Uint8List> bytes;
const DiscardSharedFileDialog({
super.key,
required this.bytes,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: FutureOrBuilder<Uint8List>(
future: bytes,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return LimitedBox(
maxHeight: 200,
maxWidth: 200,
child: FadeInImage(
fit: BoxFit.contain,
placeholder: MemoryImage(kTransparentImage),
image: MemoryImage(snapshot.data!),
),
);
},
),
title: Text(S.of(context)!.discardFile),
content: Text(
"The shared file was not yet processed. Do you want to discrad the file?", //TODO: INTL
),
actions: [
DialogCancelButton(),
DialogConfirmButton(
label: S.of(context)!.discard,
style: DialogConfirmButtonStyle.danger,
),
],
);
}
}
@@ -0,0 +1,102 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mime/mime.dart' as mime;
import 'package:printing/printing.dart';
import 'package:transparent_image/transparent_image.dart';
class FileThumbnail extends StatefulWidget {
final File? file;
final Uint8List? bytes;
final BoxFit? fit;
final double? width;
final double? height;
const FileThumbnail({
super.key,
this.file,
this.bytes,
this.fit,
this.width,
this.height,
}) : assert((bytes != null) != (file != null));
@override
State<FileThumbnail> createState() => _FileThumbnailState();
}
class _FileThumbnailState extends State<FileThumbnail> {
late String? mimeType;
@override
void initState() {
super.initState();
mimeType = widget.file != null
? mime.lookupMimeType(widget.file!.path)
: mime.lookupMimeType('', headerBytes: widget.bytes);
}
@override
Widget build(BuildContext context) {
return switch (mimeType) {
"application/pdf" => SizedBox(
width: widget.width,
height: widget.height,
child: Center(
child: FutureBuilder<Uint8List?>(
future: widget.file?.readAsBytes().then(_convertPdfToPng) ??
_convertPdfToPng(widget.bytes!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return ColoredBox(
color: Colors.white,
child: Image.memory(
snapshot.data!,
alignment: Alignment.topCenter,
fit: widget.fit,
width: widget.width,
height: widget.height,
),
);
},
),
),
),
"image/png" ||
"image/jpeg" ||
"image/tiff" ||
"image/gif" ||
"image/webp" =>
widget.file != null
? Image.file(
widget.file!,
fit: widget.fit,
width: widget.width,
height: widget.height,
)
: Image.memory(
widget.bytes!,
fit: widget.fit,
width: widget.width,
height: widget.height,
),
"text/plain" => const Center(
child: Text(".txt"),
),
_ => const Icon(Icons.file_present_outlined),
};
}
// send pdfFile as params
Future<Uint8List?> _convertPdfToPng(Uint8List bytes) async {
final info = await Printing.info();
if (!info.canRaster) {
return kTransparentImage;
}
final raster = await Printing.raster(bytes, pages: [0], dpi: 72).first;
return raster.toPng();
}
}
@@ -0,0 +1,195 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
import 'package:path/path.dart' as p;
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
class UploadQueueShell extends StatefulWidget {
final Widget child;
const UploadQueueShell({super.key, required this.child});
@override
State<UploadQueueShell> createState() => _UploadQueueShellState();
}
class _UploadQueueShellState extends State<UploadQueueShell> {
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
ReceiveSharingIntent.getInitialMedia().then(_onReceiveSharedFiles);
_subscription =
ReceiveSharingIntent.getMediaStream().listen(_onReceiveSharedFiles);
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// context.read<ReceiveShareCubit>().loadFromConsumptionDirectory(
// userId: context.read<LocalUserAccount>().id,
// );
// final state = context.read<ReceiveShareCubit>().state;
// print("Current state is " + state.toString());
// final files = state.files;
// if (files.isNotEmpty) {
// showSnackBar(
// context,
// "You have ${files.length} shared files waiting to be uploaded.",
// action: SnackBarActionConfig(
// label: "Show me",
// onPressed: () {
// UploadQueueRoute().push(context);
// },
// ),
// );
// // showDialog(
// // context: context,
// // builder: (context) => AlertDialog(
// // title: Text("Pending files"),
// // content: Text(
// // "You have ${files.length} files waiting to be uploaded.",
// // ),
// // actions: [
// // TextButton(
// // child: Text(S.of(context)!.gotIt),
// // onPressed: () {
// // Navigator.pop(context);
// // UploadQueueRoute().push(context);
// // },
// // ),
// // ],
// // ),
// // );
// }
// });
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
}
void _onTasksChanged() {
final taskNotifier = context.read<PendingTasksNotifier>();
for (var task in taskNotifier.value.values) {
context.read<LocalNotificationService>().notifyTaskChanged(task);
}
}
void _onReceiveSharedFiles(List<SharedMediaFile> sharedFiles) async {
final files = sharedFiles.map((file) => File(file.path)).toList();
if (files.isNotEmpty) {
final userId = context.read<LocalUserAccount>().id;
final notifier = context.read<ConsumptionChangeNotifier>();
await notifier.addFiles(
files: files,
userId: userId,
);
final localFiles = notifier.pendingFiles;
for (int i = 0; i < localFiles.length; i++) {
final file = localFiles[i];
await consumeLocalFile(
context,
file: file,
userId: userId,
exitAppAfterConsumed: i == localFiles.length - 1,
);
}
}
}
@override
void dispose() {
_subscription?.cancel();
context.read<PendingTasksNotifier>().removeListener(_onTasksChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
Future<void> consumeLocalFile(
BuildContext context, {
required File file,
required String userId,
bool exitAppAfterConsumed = false,
}) async {
final consumptionNotifier = context.read<ConsumptionChangeNotifier>();
final taskNotifier = context.read<PendingTasksNotifier>();
final ioFile = File(file.path);
// if (!await ioFile.exists()) {
// Fluttertoast.showToast(
// msg: S.of(context)!.couldNotAccessReceivedFile,
// toastLength: Toast.LENGTH_LONG,
// );
// }
final bytes = ioFile.readAsBytes();
final shouldDirectlyUpload =
Hive.globalSettingsBox.getValue()!.skipDocumentPreprarationOnUpload;
if (shouldDirectlyUpload) {
final taskId = await context.read<PaperlessDocumentsApi>().create(
await bytes,
filename: p.basename(file.path),
title: p.basenameWithoutExtension(file.path),
);
consumptionNotifier.discardFile(file, userId: userId);
if (taskId != null) {
taskNotifier.listenToTaskChanges(taskId);
}
} else {
final result = await DocumentUploadRoute(
$extra: bytes,
filename: p.basenameWithoutExtension(file.path),
title: p.basenameWithoutExtension(file.path),
fileExtension: p.extension(file.path),
).push<DocumentUploadResult>(context) ??
DocumentUploadResult(false, null);
if (result.success) {
await Fluttertoast.showToast(
msg: S.of(context)!.documentSuccessfullyUploadedProcessing,
);
await consumptionNotifier.discardFile(file, userId: userId);
if (result.taskId != null) {
taskNotifier.listenToTaskChanges(result.taskId!);
}
if (exitAppAfterConsumed) {
SystemNavigator.pop();
}
} else {
final shouldDiscard = await showDialog<bool>(
context: context,
builder: (context) => DiscardSharedFileDialog(bytes: bytes),
) ??
false;
if (shouldDiscard) {
await context
.read<ConsumptionChangeNotifier>()
.discardFile(file, userId: userId);
}
}
}
}