fix(raw-email): save to Downloads and show size in raw email view (#97)

- Replace getTemporaryDirectory() + OpenFilex.open() with
  getDownloadsDirectory() (fallback to temp) so the .eml file lands in
  the public Downloads folder instead of triggering Android's
  "open with" dialog.
- Show a SnackBar with the saved path after download instead of
  launching a file viewer.
- Display the email size (via fmtSize) at the top of the Raw Email
  dialog, above the scrollable content.
- Add widget test covering the size display in the Raw Email dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-15 20:39:39 +02:00
co-authored by Claude Sonnet 4.6
parent 9d19bdb81b
commit 1fa4d4911a
3 changed files with 75 additions and 9 deletions
+36 -8
View File
@@ -457,11 +457,29 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
title: const Text('Raw Email'),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
child: SelectableText(
raw,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fmtSize(raw.length),
style: Theme.of(ctx).textTheme.bodySmall?.copyWith(
color: Theme.of(ctx).colorScheme.outline,
),
),
const SizedBox(height: 4),
Flexible(
child: SingleChildScrollView(
child: SelectableText(
raw,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
],
),
),
actions: [
@@ -496,13 +514,23 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
String raw,
) async {
try {
final dir = await getTemporaryDirectory();
Directory dir;
try {
dir =
(await getDownloadsDirectory()) ?? (await getTemporaryDirectory());
} catch (_) {
dir = await getTemporaryDirectory();
}
final subject = (header?.subject ?? 'email')
.replaceAll(RegExp(r'[^\w\s-]'), '_')
.trim();
final file = File('${dir.path}/$subject.eml');
final filename = '$subject.eml';
final file = File('${dir.path}/$filename');
await file.writeAsString(raw);
await OpenFilex.open(file.path);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Saved $filename to ${dir.path}')),
);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(
+35
View File
@@ -143,6 +143,41 @@ void main() {
expect(find.text('report.pdf'), findsOneWidget);
});
testWidgets('Show Raw Email dialog shows size of email', (tester) async {
// 'A' * 2048 → fmtSize(2048) == '2.0 KB'
final rawContent = 'A' * 2048;
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/acc-1/mailboxes/INBOX/emails/acc-1%3A42',
overrides: [
accountRepositoryProvider.overrideWithValue(
FakeAccountRepository([kTestAccount]),
),
mailboxRepositoryProvider
.overrideWithValue(FakeMailboxRepository()),
emailRepositoryProvider.overrideWithValue(
FakeEmailRepository(
emailDetail: testEmail(),
emailBody:
const EmailBody(emailId: 'acc-1:42', attachments: []),
rawRfc822: rawContent,
),
),
],
),
);
await tester.pumpAndSettle();
await tester.tap(find.byType(PopupMenuButton<String>));
await tester.pumpAndSettle();
await tester.tap(find.text('Show Raw Email'));
await tester.pumpAndSettle();
expect(find.text('Raw Email'), findsOneWidget);
expect(find.text('2.0 KB'), findsOneWidget);
});
testWidgets('Show Mail Structure opens dialog with MIME parts', (
tester,
) async {
+4 -1
View File
@@ -147,6 +147,7 @@ class FakeEmailRepository implements EmailRepository {
final List<Email> _emails;
final Email? _emailDetail;
final EmailBody _emailBody;
final String _rawRfc822;
final List<Email> _searchResults;
@@ -155,9 +156,11 @@ class FakeEmailRepository implements EmailRepository {
Email? emailDetail,
EmailBody? emailBody,
List<Email>? searchResults,
String rawRfc822 = '',
}) : _emails = emails ?? [],
_emailDetail = emailDetail,
_searchResults = searchResults ?? [],
_rawRfc822 = rawRfc822,
_emailBody = emailBody ?? const EmailBody(emailId: '', attachments: []);
@override
@@ -261,7 +264,7 @@ class FakeEmailRepository implements EmailRepository {
'/tmp/${attachment.filename}';
@override
Future<String> fetchRawRfc822(String emailId) async => '';
Future<String> fetchRawRfc822(String emailId) async => _rawRfc822;
@override
Future<List<Email>> searchEmails(