From de66081813de0fe0a45ed2858344492638930a92 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Sat, 16 May 2026 17:57:31 +0200 Subject: [PATCH] feat: save raw email to temp dir and add Share action to SnackBar (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Save the .eml file to the temporary directory (reliable on all platforms) and display a Share action in the SnackBar so users can send the file to any app — including the Files app — which properly registers it with Android's MediaStore and makes it visible in the recently-used list. Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/screens/email_detail_screen.dart | 19 +++++++++++-------- pubspec.yaml | 1 + test/widget/email_detail_screen_test.dart | 11 +++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/ui/screens/email_detail_screen.dart b/lib/ui/screens/email_detail_screen.dart index 341fd11..a30a7b3 100644 --- a/lib/ui/screens/email_detail_screen.dart +++ b/lib/ui/screens/email_detail_screen.dart @@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:open_filex/open_filex.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/undo_action.dart'; @@ -517,13 +518,7 @@ class _EmailDetailScreenState extends ConsumerState { String raw, ) async { try { - Directory dir; - try { - dir = - (await getDownloadsDirectory()) ?? (await getTemporaryDirectory()); - } catch (_) { - dir = await getTemporaryDirectory(); - } + final dir = await getTemporaryDirectory(); final subject = (header?.subject ?? 'email') .replaceAll(RegExp(r'[^\w\s-]'), '_') .trim(); @@ -532,7 +527,15 @@ class _EmailDetailScreenState extends ConsumerState { await file.writeAsString(raw); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Saved $filename to ${dir.path}')), + SnackBar( + content: Text('Saved $filename'), + action: SnackBarAction( + label: 'Share', + onPressed: () => SharePlus.instance.share( + ShareParams(files: [XFile(file.path)]), + ), + ), + ), ); } catch (e) { if (!context.mounted) return; diff --git a/pubspec.yaml b/pubspec.yaml index a1a1464..a2061e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: # App version metadata for crash reports package_info_plus: ^8.0.0 + share_plus: ^12.0.2 dev_dependencies: flutter_test: diff --git a/test/widget/email_detail_screen_test.dart b/test/widget/email_detail_screen_test.dart index 093e98f..1764602 100644 --- a/test/widget/email_detail_screen_test.dart +++ b/test/widget/email_detail_screen_test.dart @@ -12,14 +12,11 @@ import 'package:sharedinbox/di.dart'; import 'helpers.dart'; -// Fake PathProviderPlatform so _downloadRaw resolves getDownloadsDirectory / -// getTemporaryDirectory via pure microtasks instead of calling xdg-user-dir. +// Fake PathProviderPlatform so _downloadRaw resolves getTemporaryDirectory +// via pure microtasks instead of calling xdg-user-dir. class _FakePathProviderPlatform extends PathProviderPlatform { @override Future getTemporaryPath() async => '/tmp'; - - @override - Future getDownloadsPath() async => '/tmp'; } // IOOverrides subclass that stubs File creation so _downloadRaw completes @@ -261,7 +258,7 @@ void main() { }); await tester.tap(find.text('Download')); - // Each pump drains one microtask level: getDownloadsDirectory, then + // Each pump drains one microtask level: getTemporaryDirectory, then // writeAsString, then _downloadRaw return, then Navigator.pop. for (var i = 0; i < 10; i++) { await tester.pump(Duration.zero); @@ -270,6 +267,8 @@ void main() { // Dialog must be dismissed after download completes. expect(find.text('Raw Email'), findsNothing); + // SnackBar with Share action must be visible. + expect(find.text('Share'), findsOneWidget); }); testWidgets('Show Mail Structure opens dialog with MIME parts', (