Complete UI gaps: move-to-folder, attachment indicators in list

- email_detail: add Move-to-folder button (bottom-sheet mailbox picker,
  excludes current folder, calls moveEmail on server)
- email_list: show amber star for flagged, paperclip for attachments
  in trailing area alongside date
- PLAN.md: mark phase 8 done
- README: add flag/move/attachment to working features

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-16 11:11:11 +02:00
co-authored by Claude Sonnet 4.6
parent 4c6c741e00
commit 6c27ad655b
4 changed files with 61 additions and 10 deletions
+1 -4
View File
@@ -26,13 +26,10 @@ UI never touches the network. The sync layer runs independently.
| 5 — SMTP send | `connectSmtp`, `EmailRepositoryImpl.sendEmail` | Done |
| 6 — UI | AccountList, AddAccount, MailboxList, EmailList, EmailDetail, Compose, Settings | Done |
| 7 — Dev tooling | Nix flake, Taskfile, Stalwart dev server, unit + integration tests, CI, pre-commit | Done |
| 8 — UI gaps | Account picker in compose, flag button, move-to-trash | **In progress** |
| 8 — UI gaps | Account picker in compose, flag/unflag, move-to-folder, attachment indicators | Done |
## Next candidates
- Account picker in compose screen (currently broken for new mail with >1 account)
- Flag / star button in email detail
- Move to trash from email detail
- Search (IMAP `SEARCH` command)
- Thread view (group by `References` / `In-Reply-To`)
- Attachment download + open
+3
View File
@@ -146,6 +146,9 @@ test/
- **Email detail** — renders plain text; falls back to HTML→plain conversion; marks as read on open; shows attachment names and sizes
- **Reply / Reply all** — pre-fills To, Subject (`Re:`), Cc from original
- **Compose** — To, Cc, Subject, Body fields; sends via SMTP
- **Flag / unflag** — star button in detail view; amber star indicator in list; synced to server
- **Move to folder** — bottom-sheet folder picker; moves on server via IMAP MOVE
- **Attachment indicators** — paperclip icon in email list; filename + size in detail
- **Delete email** — removes from server (IMAP expunge) and local DB
- **Settings** — list and remove accounts
- **Offline-first** — all reads come from local Drift/SQLite DB; network only for sync and send
+44 -3
View File
@@ -81,6 +81,11 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
if (mounted) setState(() => _isFlagged = next);
},
),
IconButton(
icon: const Icon(Icons.drive_file_move_outline),
tooltip: 'Move to folder',
onPressed: header == null ? null : () => _moveTo(context, header),
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Delete',
@@ -167,9 +172,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
final subject = (header.subject?.startsWith('Re:') ?? false)
? header.subject!
: 'Re: ${header.subject ?? ''}';
final cc = replyAll
? header.to.map((a) => a.email).join(', ')
: '';
final cc = replyAll ? header.to.map((a) => a.email).join(', ') : '';
context.push('/compose', extra: {
'replyToEmailId': widget.emailId,
'prefillTo': to,
@@ -178,4 +181,42 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
});
}
Future<void> _moveTo(BuildContext context, Email header) async {
final mailboxRepo = ref.read(mailboxRepositoryProvider);
final mailboxes =
await mailboxRepo.observeMailboxes(header.accountId).first;
// Remove the current mailbox from the list.
final destinations = mailboxes
.where((m) => m.path != header.mailboxPath)
.toList();
if (!context.mounted) return;
final chosen = await showModalBottomSheet<String>(
context: context,
builder: (ctx) => ListView(
shrinkWrap: true,
children: [
const ListTile(
title: Text(
'Move to…',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
for (final m in destinations)
ListTile(
leading: const Icon(Icons.folder_outlined),
title: Text(m.name),
onTap: () => Navigator.pop(ctx, m.path),
),
],
),
);
if (chosen == null || !context.mounted) return;
await ref.read(emailRepositoryProvider).moveEmail(widget.emailId, chosen);
if (context.mounted) context.pop();
}
}
+13 -3
View File
@@ -71,9 +71,19 @@ class EmailListScreen extends ConsumerWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
e.sentAt != null ? _dateFmt.format(e.sentAt!) : '',
style: Theme.of(ctx).textTheme.bodySmall,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (e.isFlagged)
const Icon(Icons.star, color: Colors.amber, size: 16),
if (e.hasAttachment)
const Icon(Icons.attach_file, size: 16),
const SizedBox(width: 4),
Text(
e.sentAt != null ? _dateFmt.format(e.sentAt!) : '',
style: Theme.of(ctx).textTheme.bodySmall,
),
],
),
onTap: () => context.push(
'/accounts/$accountId/mailboxes/${Uri.encodeComponent(mailboxPath)}/emails/${Uri.encodeComponent(e.id)}',