From 6eafdfdfbbbc589659f2930d361523a6c126f172 Mon Sep 17 00:00:00 2001 From: vjrj Date: Sun, 12 Jul 2026 02:56:08 +0200 Subject: [PATCH] fix(ux): filter clear pinned, no-match text, instant avatar save, market avatar, favourites, WoT drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six reported UX issues: - Inventory 'clear filters' is now pinned outside the horizontal scroll, always visible — you no longer have to scroll past every chip to find it. - Empty result now reads 'No seeds match your filters' (not 'no seeds yet') when a SEARCH or the 'this month' filter hid everything — the 'filtered' flag was missing query + sowThisMonthOnly. Guarded by a test. - Profile avatar saves + publishes the instant you pick it, so it no longer looks saved when it wasn't (the Save button sits below the fold). - Market offer detail shows the author's real avatar (photo/illustration) instead of a generic person icon — resolves + caches their kind:0 picture. - 'Wishlist' renamed to Favourites/Favoritos (en/es/pt/ast). - The drawer 'following' item now opens the web-of-trust 'your people' screen when the social layer is on (was a dead item). analyze clean; inventory/market/home + overflow suites green. --- apps/app_seeds/lib/i18n/ast.i18n.json | 2 +- apps/app_seeds/lib/i18n/en.i18n.json | 2 +- apps/app_seeds/lib/i18n/es.i18n.json | 2 +- apps/app_seeds/lib/i18n/pt.i18n.json | 2 +- apps/app_seeds/lib/i18n/strings.g.dart | 2 +- apps/app_seeds/lib/i18n/strings_ast.g.dart | 4 +-- apps/app_seeds/lib/i18n/strings_en.g.dart | 6 ++-- apps/app_seeds/lib/i18n/strings_es.g.dart | 4 +-- apps/app_seeds/lib/i18n/strings_pt.g.dart | 4 +-- apps/app_seeds/lib/ui/app_drawer.dart | 8 +++++ .../lib/ui/inventory_list_screen.dart | 35 ++++++++++++------- .../lib/ui/market_offer_detail_screen.dart | 30 ++++++++++++---- apps/app_seeds/lib/ui/profile_screen.dart | 6 +++- .../test/ui/inventory_list_screen_test.dart | 17 +++++++++ 14 files changed, 91 insertions(+), 33 deletions(-) diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json index c95aeab..4890ef5 100644 --- a/apps/app_seeds/lib/i18n/ast.i18n.json +++ b/apps/app_seeds/lib/i18n/ast.i18n.json @@ -74,7 +74,7 @@ "market": "Mercáu", "profile": "El to perfil", "chat": "Charra", - "wishlist": "Llista de deseos", + "wishlist": "Favoritos", "following": "Siguiendo", "plantares": "Plantares", "sales": "Ventes", diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 994bbb8..895c1ba 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -75,7 +75,7 @@ "market": "Market", "profile": "Your profile", "chat": "Chat", - "wishlist": "Wishlist", + "wishlist": "Favorites", "following": "Following", "plantares": "Plantares", "sales": "Sales", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 88e26f5..a9aab6a 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -75,7 +75,7 @@ "market": "Mercado", "profile": "Tu perfil", "chat": "Chat", - "wishlist": "Lista de deseos", + "wishlist": "Favoritos", "following": "Siguiendo", "plantares": "Plantares", "sales": "Ventas", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index fc10428..7b60c37 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -75,7 +75,7 @@ "market": "Mercado", "profile": "O teu perfil", "chat": "Conversas", - "wishlist": "Lista de desejos", + "wishlist": "Favoritos", "following": "A seguir", "plantares": "Plantares", "sales": "Vendas", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index efa6566..9800d6f 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 4 /// Strings: 2032 (508 per locale) /// -/// Built on 2026-07-11 at 20:16 UTC +/// Built on 2026-07-12 at 00:47 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_ast.g.dart b/apps/app_seeds/lib/i18n/strings_ast.g.dart index 6e90aed..dc79065 100644 --- a/apps/app_seeds/lib/i18n/strings_ast.g.dart +++ b/apps/app_seeds/lib/i18n/strings_ast.g.dart @@ -223,7 +223,7 @@ class _Translations$menu$ast extends Translations$menu$en { @override String get market => 'Mercáu'; @override String get profile => 'El to perfil'; @override String get chat => 'Charra'; - @override String get wishlist => 'Llista de deseos'; + @override String get wishlist => 'Favoritos'; @override String get following => 'Siguiendo'; @override String get plantares => 'Plantares'; @override String get sales => 'Ventes'; @@ -1366,7 +1366,7 @@ extension on TranslationsAst { 'menu.market' => 'Mercáu', 'menu.profile' => 'El to perfil', 'menu.chat' => 'Charra', - 'menu.wishlist' => 'Llista de deseos', + 'menu.wishlist' => 'Favoritos', 'menu.following' => 'Siguiendo', 'menu.plantares' => 'Plantares', 'menu.sales' => 'Ventes', diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 0326197..a7af8e4 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -344,8 +344,8 @@ class Translations$menu$en { /// en: 'Chat' String get chat => 'Chat'; - /// en: 'Wishlist' - String get wishlist => 'Wishlist'; + /// en: 'Favorites' + String get wishlist => 'Favorites'; /// en: 'Following' String get following => 'Following'; @@ -2370,7 +2370,7 @@ extension on Translations { 'menu.market' => 'Market', 'menu.profile' => 'Your profile', 'menu.chat' => 'Chat', - 'menu.wishlist' => 'Wishlist', + 'menu.wishlist' => 'Favorites', 'menu.following' => 'Following', 'menu.plantares' => 'Plantares', 'menu.sales' => 'Sales', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 712e6f7..e1b6184 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -224,7 +224,7 @@ class _Translations$menu$es extends Translations$menu$en { @override String get market => 'Mercado'; @override String get profile => 'Tu perfil'; @override String get chat => 'Chat'; - @override String get wishlist => 'Lista de deseos'; + @override String get wishlist => 'Favoritos'; @override String get following => 'Siguiendo'; @override String get plantares => 'Plantares'; @override String get sales => 'Ventas'; @@ -1369,7 +1369,7 @@ extension on TranslationsEs { 'menu.market' => 'Mercado', 'menu.profile' => 'Tu perfil', 'menu.chat' => 'Chat', - 'menu.wishlist' => 'Lista de deseos', + 'menu.wishlist' => 'Favoritos', 'menu.following' => 'Siguiendo', 'menu.plantares' => 'Plantares', 'menu.sales' => 'Ventas', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 36cb37a..50c1c35 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -224,7 +224,7 @@ class _Translations$menu$pt extends Translations$menu$en { @override String get market => 'Mercado'; @override String get profile => 'O teu perfil'; @override String get chat => 'Conversas'; - @override String get wishlist => 'Lista de desejos'; + @override String get wishlist => 'Favoritos'; @override String get following => 'A seguir'; @override String get plantares => 'Plantares'; @override String get sales => 'Vendas'; @@ -1366,7 +1366,7 @@ extension on TranslationsPt { 'menu.market' => 'Mercado', 'menu.profile' => 'O teu perfil', 'menu.chat' => 'Conversas', - 'menu.wishlist' => 'Lista de desejos', + 'menu.wishlist' => 'Favoritos', 'menu.following' => 'A seguir', 'menu.plantares' => 'Plantares', 'menu.sales' => 'Vendas', diff --git a/apps/app_seeds/lib/ui/app_drawer.dart b/apps/app_seeds/lib/ui/app_drawer.dart index 135878f..a890885 100644 --- a/apps/app_seeds/lib/ui/app_drawer.dart +++ b/apps/app_seeds/lib/ui/app_drawer.dart @@ -104,9 +104,17 @@ class AppDrawer extends StatelessWidget { icon: const Icon(Icons.favorite), label: t.menu.wishlist, ), + // The ego-centric web of trust ("your people") — live once the + // social layer is on. _DrawerItem( icon: const Icon(Icons.group), label: t.menu.following, + onTap: marketEnabled + ? () { + Navigator.of(context).pop(); + context.push('/your-people'); + } + : null, ), ], ), diff --git a/apps/app_seeds/lib/ui/inventory_list_screen.dart b/apps/app_seeds/lib/ui/inventory_list_screen.dart index dddb729..db1affd 100644 --- a/apps/app_seeds/lib/ui/inventory_list_screen.dart +++ b/apps/app_seeds/lib/ui/inventory_list_screen.dart @@ -85,13 +85,16 @@ class InventoryListScreen extends StatelessWidget { items: state.visibleItems, selectionMode: state.selectionMode, selectedIds: state.selectedIds, - // Distinguish "no seeds at all" from "filters hid them". + // Distinguish "no seeds at all" from "search/filters + // hid them" — so an empty result reads "no matches". filtered: + state.query.trim().isNotEmpty || state.categoryFilter.isNotEmpty || state.typeFilter.isNotEmpty || state.organicOnly || state.needsReproductionOnly || - state.sharingOnly, + state.sharingOnly || + state.sowThisMonthOnly, ), ), ], @@ -412,22 +415,30 @@ class _FilterBar extends StatelessWidget { )); } } - if (hasActiveFilter) { - row.add(TextButton.icon( - key: const Key('inventory.filter.clear'), - onPressed: cubit.clearFilters, - icon: const Icon(Icons.clear, size: 18), - label: Text(t.inventory.clearFilters), - )); - } - - return EdgeFade( + final scroller = EdgeFade( child: SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row(children: row), ), ); + if (!hasActiveFilter) return scroller; + + // Pin "clear filters" outside the scroll so it's always visible — you + // shouldn't have to scroll past every chip to discover it exists. + return Row( + children: [ + Expanded(child: scroller), + const _FilterGroupDivider(), + TextButton.icon( + key: const Key('inventory.filter.clear'), + onPressed: cubit.clearFilters, + icon: const Icon(Icons.filter_alt_off_outlined, size: 18), + label: Text(t.inventory.clearFilters), + ), + const SizedBox(width: 4), + ], + ); } } diff --git a/apps/app_seeds/lib/ui/market_offer_detail_screen.dart b/apps/app_seeds/lib/ui/market_offer_detail_screen.dart index 1ccc1dd..d078830 100644 --- a/apps/app_seeds/lib/ui/market_offer_detail_screen.dart +++ b/apps/app_seeds/lib/ui/market_offer_detail_screen.dart @@ -11,6 +11,7 @@ import '../services/social_connection.dart'; import '../services/social_service.dart'; import '../state/peer_rating_cubit.dart'; import 'market_widgets.dart'; +import 'peer_avatar.dart'; import 'rating_sheet.dart'; import 'theme.dart'; @@ -53,6 +54,7 @@ class _MarketOfferDetailScreenState extends State { String? _sellerName; String? _sellerAbout; String? _sellerG1; + String? _sellerPicture; bool _profileLoading = true; PeerRatingState? _sellerRating; @@ -66,7 +68,14 @@ class _MarketOfferDetailScreenState extends State { /// same path the chat header uses. Degrades silently when offline. Future _loadSeller() async { final cachedName = await widget.profileCache?.name(widget.offer.authorPubkeyHex); - if (mounted && cachedName != null) setState(() => _sellerName = cachedName); + final cachedPic = + await widget.profileCache?.picture(widget.offer.authorPubkeyHex); + if (mounted) { + setState(() { + if (cachedName != null) _sellerName = cachedName; + if (cachedPic != null) _sellerPicture = cachedPic; + }); + } final session = await widget.connection.session(); if (!mounted) return; @@ -81,11 +90,18 @@ class _MarketOfferDetailScreenState extends State { unawaited(widget.profileCache ?.setName(widget.offer.authorPubkeyHex, profile.name)); } + if (profile != null && profile.picture.isNotEmpty) { + unawaited(widget.profileCache + ?.setPicture(widget.offer.authorPubkeyHex, profile.picture)); + } if (mounted) { setState(() { if (profile != null && profile.name.isNotEmpty) { _sellerName = profile.name; } + if (profile != null && profile.picture.isNotEmpty) { + _sellerPicture = profile.picture; + } _sellerAbout = profile?.about.isNotEmpty == true ? profile!.about : null; _sellerG1 = profile?.g1.isNotEmpty == true ? profile!.g1 : null; _profileLoading = false; @@ -194,7 +210,9 @@ class _MarketOfferDetailScreenState extends State { const SizedBox(height: 12), _SellerSection( title: t.market.sharedBy, + pubkey: o.authorPubkeyHex, name: _sellerName ?? shortPubkey(o.authorPubkeyHex), + picture: _sellerPicture, about: _sellerAbout, rating: _sellerRating, loading: _profileLoading, @@ -250,6 +268,7 @@ class _Badge extends StatelessWidget { class _SellerSection extends StatelessWidget { const _SellerSection({ required this.title, + required this.pubkey, required this.name, required this.about, required this.loading, @@ -257,11 +276,14 @@ class _SellerSection extends StatelessWidget { required this.noProfileLabel, required this.onCopyId, required this.copyIdTooltip, + this.picture, this.rating, }); final String title; + final String pubkey; final String name; + final String? picture; final String? about; /// Circle-weighted summary of the author's ratings; stars show only when @@ -289,11 +311,7 @@ class _SellerSection extends StatelessWidget { const SizedBox(height: 8), Row( children: [ - const CircleAvatar( - radius: 20, - backgroundColor: seedPrimaryContainer, - child: Icon(Icons.person_outline, color: seedGreen), - ), + PeerAvatar(pubkey: pubkey, name: name, picture: picture, radius: 20), const SizedBox(width: 12), Expanded( child: Column( diff --git a/apps/app_seeds/lib/ui/profile_screen.dart b/apps/app_seeds/lib/ui/profile_screen.dart index 6231c03..8efb586 100644 --- a/apps/app_seeds/lib/ui/profile_screen.dart +++ b/apps/app_seeds/lib/ui/profile_screen.dart @@ -114,7 +114,11 @@ class _ProfileScreenState extends State { Future _editAvatar() async { final result = await showAvatarPicker(context, current: _avatar); - if (result != null && mounted) setState(() => _avatar = result); + if (result == null || !mounted) return; + setState(() => _avatar = result); + // Persist + publish right away: an avatar change shouldn't depend on + // finding the Save button further down the screen. + await _save(); } Future _copyId() async { diff --git a/apps/app_seeds/test/ui/inventory_list_screen_test.dart b/apps/app_seeds/test/ui/inventory_list_screen_test.dart index 236c2d7..47932e7 100644 --- a/apps/app_seeds/test/ui/inventory_list_screen_test.dart +++ b/apps/app_seeds/test/ui/inventory_list_screen_test.dart @@ -35,6 +35,23 @@ void main() { await disposeTree(tester); }); + testWidgets('a search with no matches reads "no matches", not "no seeds yet"', + (tester) async { + final repo = newTestRepository(db); + await repo.addQuickVariety(label: 'Tomate', category: 'Solanaceae'); + await tester.pumpWidget( + wrapScreen(repository: repo, child: const InventoryListScreen()), + ); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key('inventory.search')), 'zzzz'); + await tester.pumpAndSettle(); + + expect(find.text('No seeds match your filters.'), findsOneWidget); + expect(find.text('No seeds yet. Tap + to add your first.'), findsNothing); + await disposeTree(tester); + }); + testWidgets( 'renders seeds from the repository grouped under their category', (tester) async {