From 58f4b13c6ac192ddc63d373deeff5375c44f6728 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Mon, 11 May 2026 08:35:38 +0200 Subject: [PATCH] ui: stabilize email list during selection (Issue #14) --- lib/ui/screens/email_list_screen.dart | 191 +++++++++++++------------- 1 file changed, 98 insertions(+), 93 deletions(-) diff --git a/lib/ui/screens/email_list_screen.dart b/lib/ui/screens/email_list_screen.dart index fde0cfc..a4f1685 100644 --- a/lib/ui/screens/email_list_screen.dart +++ b/lib/ui/screens/email_list_screen.dart @@ -125,7 +125,7 @@ class _EmailListScreenState extends ConsumerState { final accountAsync = ref.watch(accountByIdProvider(widget.accountId)); return Scaffold( - appBar: _selecting ? _selectionBar() : _normalBar(repo, accountAsync), + appBar: _buildAppBar(repo, accountAsync), drawer: _selecting ? null : FolderDrawer( @@ -139,50 +139,63 @@ class _EmailListScreenState extends ConsumerState { ); } - AppBar _normalBar( + PreferredSizeWidget _buildAppBar( EmailRepository emailRepo, AsyncValue accountAsync, ) { + final selectionCount = + _searching ? _selectedSearchIds.length : _selectedThreadIds.length; + return AppBar( - title: Text(widget.mailboxPath), - actions: [ - accountAsync.when( - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - data: (account) => Padding( - padding: const EdgeInsets.only(right: 4), - child: Center( - child: Text( - account?.displayName ?? '', - style: Theme.of(context).textTheme.bodySmall, + leading: _selecting + ? IconButton( + icon: const Icon(Icons.close), + onPressed: _clearSelection, + ) + : null, + title: _selecting + ? Text('$selectionCount selected') + : Text(widget.mailboxPath), + actions: _selecting + ? [] + : [ + accountAsync.when( + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + data: (account) => Padding( + padding: const EdgeInsets.only(right: 4), + child: Center( + child: Text( + account?.displayName ?? '', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), ), - ), - ), - ), - IconButton( - icon: const Icon(Icons.sync), - onPressed: () async { - try { - await emailRepo.syncEmails( - widget.accountId, - widget.mailboxPath, - ); - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Sync failed: $e')), - ); - } - }, - ), - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => context.push( - '/compose', - extra: {'accountId': widget.accountId}, - ), - ), - ], + IconButton( + icon: const Icon(Icons.sync), + onPressed: () async { + try { + await emailRepo.syncEmails( + widget.accountId, + widget.mailboxPath, + ); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Sync failed: $e')), + ); + } + }, + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => context.push( + '/compose', + extra: {'accountId': widget.accountId}, + ), + ), + ], bottom: PreferredSize( preferredSize: const Size.fromHeight(60), child: Padding( @@ -191,8 +204,9 @@ class _EmailListScreenState extends ConsumerState { controller: _searchController, hintText: 'Search…', leading: const Icon(Icons.search), + enabled: !_selecting, trailing: [ - if (_searchController.text.isNotEmpty) + if (_searchController.text.isNotEmpty && !_selecting) IconButton( icon: const Icon(Icons.clear), onPressed: () => _searchController.clear(), @@ -207,18 +221,6 @@ class _EmailListScreenState extends ConsumerState { ); } - AppBar _selectionBar() { - final count = - _searching ? _selectedSearchIds.length : _selectedThreadIds.length; - return AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: _clearSelection, - ), - title: Text('$count selected'), - ); - } - Widget _selectionBottomBar() { return BottomAppBar( child: Row( @@ -484,15 +486,19 @@ class _EmailListScreenState extends ConsumerState { t.participants.map((a) => a.name ?? a.email).take(3).join(', '); final tile = ListTile( - leading: _selecting - ? Checkbox( - value: isSelected, - onChanged: (_) => _toggleThreadSelection(t), - ) - : Icon( - t.hasUnread ? Icons.mail : Icons.mail_outline, - color: t.hasUnread ? Theme.of(ctx).colorScheme.primary : null, - ), + leading: SizedBox( + width: 40, + child: _selecting + ? Checkbox( + value: isSelected, + onChanged: (_) => _toggleThreadSelection(t), + ) + : Icon( + t.hasUnread ? Icons.mail : Icons.mail_outline, + color: + t.hasUnread ? Theme.of(ctx).colorScheme.primary : null, + ), + ), title: Row( children: [ Expanded( @@ -535,20 +541,18 @@ class _EmailListScreenState extends ConsumerState { ], ), selected: isSelected, - trailing: _selecting - ? null - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (t.isFlagged) - const Icon(Icons.star, color: Colors.amber, size: 16), - const SizedBox(width: 4), - Text( - _dateFmt.format(t.latestDate), - style: Theme.of(ctx).textTheme.bodySmall, - ), - ], - ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (t.isFlagged) + const Icon(Icons.star, color: Colors.amber, size: 16), + const SizedBox(width: 4), + Text( + _dateFmt.format(t.latestDate), + style: Theme.of(ctx).textTheme.bodySmall, + ), + ], + ), onTap: _selecting ? () => _toggleThreadSelection(t) : t.messageCount > 1 @@ -561,12 +565,12 @@ class _EmailListScreenState extends ConsumerState { onLongPress: () => _toggleThreadSelection(t), ); - if (_selecting) return tile; - // For swipe actions on threads, operate on the latest email only // (single-email threads) or the whole thread. return Dismissible( key: ValueKey(t.threadId), + direction: + _selecting ? DismissDirection.none : DismissDirection.horizontal, background: _swipeBackground( alignment: Alignment.centerLeft, color: Colors.green, @@ -646,15 +650,18 @@ class _EmailListScreenState extends ConsumerState { ? (e.from.first.name ?? e.from.first.email) : '(unknown)'; return ListTile( - leading: _selecting - ? Checkbox( - value: isSelected, - onChanged: (_) => _toggleSearchSelection(e.id), - ) - : Icon( - e.isSeen ? Icons.mail_outline : Icons.mail, - color: e.isSeen ? null : Theme.of(ctx).colorScheme.primary, - ), + leading: SizedBox( + width: 40, + child: _selecting + ? Checkbox( + 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: @@ -666,12 +673,10 @@ class _EmailListScreenState extends ConsumerState { overflow: TextOverflow.ellipsis, ), selected: isSelected, - trailing: _selecting - ? null - : Text( - e.sentAt != null ? _dateFmt.format(e.sentAt!) : '', - style: Theme.of(ctx).textTheme.bodySmall, - ), + trailing: Text( + e.sentAt != null ? _dateFmt.format(e.sentAt!) : '', + style: Theme.of(ctx).textTheme.bodySmall, + ), onTap: _selecting ? () => _toggleSearchSelection(e.id) : () => context.push(