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:
co-authored by
Claude Sonnet 4.6
parent
4c6c741e00
commit
6c27ad655b
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}',
|
||||
|
||||
Reference in New Issue
Block a user