From 3125713e6b858ec93fed15f08c186401b7ef1964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bot=20of=20Thomas=20G=C3=BCttler?= Date: Thu, 14 May 2026 05:20:11 +0200 Subject: [PATCH] refactor(A2): extract shared EmailTile widget (#33) --- lib/ui/screens/email_list_screen.dart | 28 ++-------- lib/ui/screens/search_screen.dart | 50 ++++-------------- lib/ui/widgets/email_tile.dart | 74 +++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 lib/ui/widgets/email_tile.dart diff --git a/lib/ui/screens/email_list_screen.dart b/lib/ui/screens/email_list_screen.dart index 817d07b..6fc4f38 100644 --- a/lib/ui/screens/email_list_screen.dart +++ b/lib/ui/screens/email_list_screen.dart @@ -10,6 +10,7 @@ import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/undo_action.dart'; import 'package:sharedinbox/core/repositories/email_repository.dart'; import 'package:sharedinbox/di.dart'; +import 'package:sharedinbox/ui/widgets/email_tile.dart'; import 'package:sharedinbox/ui/widgets/folder_drawer.dart'; import 'package:sharedinbox/ui/widgets/snooze_picker.dart'; @@ -711,10 +712,9 @@ class _EmailListScreenState extends ConsumerState { itemBuilder: (ctx, i) { final e = emails[i]; final isSelected = _selectedSearchIds.contains(e.id); - final sender = e.from.isNotEmpty - ? (e.from.first.name ?? e.from.first.email) - : '(unknown)'; - return ListTile( + return EmailTile( + email: e, + selected: isSelected, leading: SizedBox( width: 40, child: _selecting @@ -722,25 +722,7 @@ class _EmailListScreenState extends ConsumerState { value: isSelected, onChanged: (_) => _toggleSearchSelection(e.id), ) - : Icon( - e.isSeen ? Icons.mail_outline : Icons.mail, - color: e.isSeen ? null : Theme.of(ctx).colorScheme.primary, - ), - ), - title: Text( - sender, - style: - e.isSeen ? null : const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - e.subject ?? '(no subject)', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - selected: isSelected, - trailing: Text( - e.sentAt != null ? _dateFmt.format(e.sentAt!) : '', - style: Theme.of(ctx).textTheme.bodySmall, + : null, ), onTap: _selecting ? () => _toggleSearchSelection(e.id) diff --git a/lib/ui/screens/search_screen.dart b/lib/ui/screens/search_screen.dart index 1b7ea55..00f9d7e 100644 --- a/lib/ui/screens/search_screen.dart +++ b/lib/ui/screens/search_screen.dart @@ -8,6 +8,7 @@ import 'package:sharedinbox/core/models/email.dart'; import 'package:sharedinbox/core/models/mailbox.dart'; import 'package:sharedinbox/core/utils/logger.dart'; import 'package:sharedinbox/di.dart'; +import 'package:sharedinbox/ui/widgets/email_tile.dart'; class SearchScreen extends ConsumerStatefulWidget { const SearchScreen({super.key, this.accountId}); @@ -155,7 +156,15 @@ class _SearchScreenState extends ConsumerState { if (r.emails.isNotEmpty) ...[ const _SectionHeader('Messages'), for (final e in r.emails) - _EmailTile(email: e, accountId: e.accountId), + EmailTile( + email: e, + showLocation: true, + onTap: () => context.push( + '/accounts/${e.accountId}/mailboxes' + '/${Uri.encodeComponent(e.mailboxPath)}' + '/emails/${Uri.encodeComponent(e.id)}', + ), + ), ], ], ); @@ -246,42 +255,3 @@ class _AddressTile extends StatelessWidget { ); } } - -class _EmailTile extends StatelessWidget { - const _EmailTile({required this.email, required this.accountId}); - final Email email; - final String accountId; - - @override - Widget build(BuildContext context) { - final sender = email.from.isNotEmpty - ? (email.from.first.name ?? email.from.first.email) - : '(unknown)'; - return ListTile( - leading: Icon( - email.isSeen ? Icons.mail_outline : Icons.mail, - color: email.isSeen ? null : Theme.of(context).colorScheme.primary, - ), - title: Text(sender), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - email.subject ?? '(no subject)', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - '$accountId • ${email.mailboxPath}', - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - onTap: () => context.push( - '/accounts/$accountId/mailboxes' - '/${Uri.encodeComponent(email.mailboxPath)}' - '/emails/${Uri.encodeComponent(email.id)}', - ), - ); - } -} diff --git a/lib/ui/widgets/email_tile.dart b/lib/ui/widgets/email_tile.dart new file mode 100644 index 0000000..d8d5794 --- /dev/null +++ b/lib/ui/widgets/email_tile.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import 'package:sharedinbox/core/models/email.dart'; + +final _dateFmt = DateFormat('MMM d'); + +/// A flat list tile for an individual [email]. +/// +/// Used in search-result lists and the per-mailbox search overlay. +/// Pass a custom [leading] widget to support selection-mode checkboxes. +class EmailTile extends StatelessWidget { + const EmailTile({ + super.key, + required this.email, + required this.onTap, + this.leading, + this.selected = false, + this.onLongPress, + this.showLocation = false, + }); + + final Email email; + final VoidCallback onTap; + final Widget? leading; + final bool selected; + final VoidCallback? onLongPress; + + /// When true, appends `accountId • mailboxPath` as a second subtitle line. + final bool showLocation; + + @override + Widget build(BuildContext context) { + final sender = email.from.isNotEmpty + ? (email.from.first.name ?? email.from.first.email) + : '(unknown)'; + final date = email.sentAt != null ? _dateFmt.format(email.sentAt!) : ''; + + return ListTile( + leading: leading ?? + Icon( + email.isSeen ? Icons.mail_outline : Icons.mail, + color: email.isSeen ? null : Theme.of(context).colorScheme.primary, + ), + title: Text( + sender, + style: + email.isSeen ? null : const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + email.subject ?? '(no subject)', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (showLocation) + Text( + '${email.accountId} • ${email.mailboxPath}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + trailing: date.isEmpty + ? null + : Text(date, style: Theme.of(context).textTheme.bodySmall), + selected: selected, + onTap: onTap, + onLongPress: onLongPress, + ); + } +}