fix(ux): filter clear pinned, no-match text, instant avatar save, market avatar, favourites, WoT drawer
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.
This commit is contained in:
parent
51d6924464
commit
6eafdfdfbb
14 changed files with 91 additions and 33 deletions
|
|
@ -74,7 +74,7 @@
|
||||||
"market": "Mercáu",
|
"market": "Mercáu",
|
||||||
"profile": "El to perfil",
|
"profile": "El to perfil",
|
||||||
"chat": "Charra",
|
"chat": "Charra",
|
||||||
"wishlist": "Llista de deseos",
|
"wishlist": "Favoritos",
|
||||||
"following": "Siguiendo",
|
"following": "Siguiendo",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
"sales": "Ventes",
|
"sales": "Ventes",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
"market": "Market",
|
"market": "Market",
|
||||||
"profile": "Your profile",
|
"profile": "Your profile",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
"wishlist": "Wishlist",
|
"wishlist": "Favorites",
|
||||||
"following": "Following",
|
"following": "Following",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
"sales": "Sales",
|
"sales": "Sales",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
"market": "Mercado",
|
"market": "Mercado",
|
||||||
"profile": "Tu perfil",
|
"profile": "Tu perfil",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
"wishlist": "Lista de deseos",
|
"wishlist": "Favoritos",
|
||||||
"following": "Siguiendo",
|
"following": "Siguiendo",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
"sales": "Ventas",
|
"sales": "Ventas",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
"market": "Mercado",
|
"market": "Mercado",
|
||||||
"profile": "O teu perfil",
|
"profile": "O teu perfil",
|
||||||
"chat": "Conversas",
|
"chat": "Conversas",
|
||||||
"wishlist": "Lista de desejos",
|
"wishlist": "Favoritos",
|
||||||
"following": "A seguir",
|
"following": "A seguir",
|
||||||
"plantares": "Plantares",
|
"plantares": "Plantares",
|
||||||
"sales": "Vendas",
|
"sales": "Vendas",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
/// Locales: 4
|
/// Locales: 4
|
||||||
/// Strings: 2032 (508 per locale)
|
/// 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
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ class _Translations$menu$ast extends Translations$menu$en {
|
||||||
@override String get market => 'Mercáu';
|
@override String get market => 'Mercáu';
|
||||||
@override String get profile => 'El to perfil';
|
@override String get profile => 'El to perfil';
|
||||||
@override String get chat => 'Charra';
|
@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 following => 'Siguiendo';
|
||||||
@override String get plantares => 'Plantares';
|
@override String get plantares => 'Plantares';
|
||||||
@override String get sales => 'Ventes';
|
@override String get sales => 'Ventes';
|
||||||
|
|
@ -1366,7 +1366,7 @@ extension on TranslationsAst {
|
||||||
'menu.market' => 'Mercáu',
|
'menu.market' => 'Mercáu',
|
||||||
'menu.profile' => 'El to perfil',
|
'menu.profile' => 'El to perfil',
|
||||||
'menu.chat' => 'Charra',
|
'menu.chat' => 'Charra',
|
||||||
'menu.wishlist' => 'Llista de deseos',
|
'menu.wishlist' => 'Favoritos',
|
||||||
'menu.following' => 'Siguiendo',
|
'menu.following' => 'Siguiendo',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
'menu.sales' => 'Ventes',
|
'menu.sales' => 'Ventes',
|
||||||
|
|
|
||||||
|
|
@ -344,8 +344,8 @@ class Translations$menu$en {
|
||||||
/// en: 'Chat'
|
/// en: 'Chat'
|
||||||
String get chat => 'Chat';
|
String get chat => 'Chat';
|
||||||
|
|
||||||
/// en: 'Wishlist'
|
/// en: 'Favorites'
|
||||||
String get wishlist => 'Wishlist';
|
String get wishlist => 'Favorites';
|
||||||
|
|
||||||
/// en: 'Following'
|
/// en: 'Following'
|
||||||
String get following => 'Following';
|
String get following => 'Following';
|
||||||
|
|
@ -2370,7 +2370,7 @@ extension on Translations {
|
||||||
'menu.market' => 'Market',
|
'menu.market' => 'Market',
|
||||||
'menu.profile' => 'Your profile',
|
'menu.profile' => 'Your profile',
|
||||||
'menu.chat' => 'Chat',
|
'menu.chat' => 'Chat',
|
||||||
'menu.wishlist' => 'Wishlist',
|
'menu.wishlist' => 'Favorites',
|
||||||
'menu.following' => 'Following',
|
'menu.following' => 'Following',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
'menu.sales' => 'Sales',
|
'menu.sales' => 'Sales',
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ class _Translations$menu$es extends Translations$menu$en {
|
||||||
@override String get market => 'Mercado';
|
@override String get market => 'Mercado';
|
||||||
@override String get profile => 'Tu perfil';
|
@override String get profile => 'Tu perfil';
|
||||||
@override String get chat => 'Chat';
|
@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 following => 'Siguiendo';
|
||||||
@override String get plantares => 'Plantares';
|
@override String get plantares => 'Plantares';
|
||||||
@override String get sales => 'Ventas';
|
@override String get sales => 'Ventas';
|
||||||
|
|
@ -1369,7 +1369,7 @@ extension on TranslationsEs {
|
||||||
'menu.market' => 'Mercado',
|
'menu.market' => 'Mercado',
|
||||||
'menu.profile' => 'Tu perfil',
|
'menu.profile' => 'Tu perfil',
|
||||||
'menu.chat' => 'Chat',
|
'menu.chat' => 'Chat',
|
||||||
'menu.wishlist' => 'Lista de deseos',
|
'menu.wishlist' => 'Favoritos',
|
||||||
'menu.following' => 'Siguiendo',
|
'menu.following' => 'Siguiendo',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
'menu.sales' => 'Ventas',
|
'menu.sales' => 'Ventas',
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ class _Translations$menu$pt extends Translations$menu$en {
|
||||||
@override String get market => 'Mercado';
|
@override String get market => 'Mercado';
|
||||||
@override String get profile => 'O teu perfil';
|
@override String get profile => 'O teu perfil';
|
||||||
@override String get chat => 'Conversas';
|
@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 following => 'A seguir';
|
||||||
@override String get plantares => 'Plantares';
|
@override String get plantares => 'Plantares';
|
||||||
@override String get sales => 'Vendas';
|
@override String get sales => 'Vendas';
|
||||||
|
|
@ -1366,7 +1366,7 @@ extension on TranslationsPt {
|
||||||
'menu.market' => 'Mercado',
|
'menu.market' => 'Mercado',
|
||||||
'menu.profile' => 'O teu perfil',
|
'menu.profile' => 'O teu perfil',
|
||||||
'menu.chat' => 'Conversas',
|
'menu.chat' => 'Conversas',
|
||||||
'menu.wishlist' => 'Lista de desejos',
|
'menu.wishlist' => 'Favoritos',
|
||||||
'menu.following' => 'A seguir',
|
'menu.following' => 'A seguir',
|
||||||
'menu.plantares' => 'Plantares',
|
'menu.plantares' => 'Plantares',
|
||||||
'menu.sales' => 'Vendas',
|
'menu.sales' => 'Vendas',
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,17 @@ class AppDrawer extends StatelessWidget {
|
||||||
icon: const Icon(Icons.favorite),
|
icon: const Icon(Icons.favorite),
|
||||||
label: t.menu.wishlist,
|
label: t.menu.wishlist,
|
||||||
),
|
),
|
||||||
|
// The ego-centric web of trust ("your people") — live once the
|
||||||
|
// social layer is on.
|
||||||
_DrawerItem(
|
_DrawerItem(
|
||||||
icon: const Icon(Icons.group),
|
icon: const Icon(Icons.group),
|
||||||
label: t.menu.following,
|
label: t.menu.following,
|
||||||
|
onTap: marketEnabled
|
||||||
|
? () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.push('/your-people');
|
||||||
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -85,13 +85,16 @@ class InventoryListScreen extends StatelessWidget {
|
||||||
items: state.visibleItems,
|
items: state.visibleItems,
|
||||||
selectionMode: state.selectionMode,
|
selectionMode: state.selectionMode,
|
||||||
selectedIds: state.selectedIds,
|
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:
|
filtered:
|
||||||
|
state.query.trim().isNotEmpty ||
|
||||||
state.categoryFilter.isNotEmpty ||
|
state.categoryFilter.isNotEmpty ||
|
||||||
state.typeFilter.isNotEmpty ||
|
state.typeFilter.isNotEmpty ||
|
||||||
state.organicOnly ||
|
state.organicOnly ||
|
||||||
state.needsReproductionOnly ||
|
state.needsReproductionOnly ||
|
||||||
state.sharingOnly,
|
state.sharingOnly ||
|
||||||
|
state.sowThisMonthOnly,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -412,22 +415,30 @@ class _FilterBar extends StatelessWidget {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasActiveFilter) {
|
final scroller = EdgeFade(
|
||||||
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(
|
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(children: row),
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import '../services/social_connection.dart';
|
||||||
import '../services/social_service.dart';
|
import '../services/social_service.dart';
|
||||||
import '../state/peer_rating_cubit.dart';
|
import '../state/peer_rating_cubit.dart';
|
||||||
import 'market_widgets.dart';
|
import 'market_widgets.dart';
|
||||||
|
import 'peer_avatar.dart';
|
||||||
import 'rating_sheet.dart';
|
import 'rating_sheet.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
|
|
@ -53,6 +54,7 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
||||||
String? _sellerName;
|
String? _sellerName;
|
||||||
String? _sellerAbout;
|
String? _sellerAbout;
|
||||||
String? _sellerG1;
|
String? _sellerG1;
|
||||||
|
String? _sellerPicture;
|
||||||
bool _profileLoading = true;
|
bool _profileLoading = true;
|
||||||
PeerRatingState? _sellerRating;
|
PeerRatingState? _sellerRating;
|
||||||
|
|
||||||
|
|
@ -66,7 +68,14 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
||||||
/// same path the chat header uses. Degrades silently when offline.
|
/// same path the chat header uses. Degrades silently when offline.
|
||||||
Future<void> _loadSeller() async {
|
Future<void> _loadSeller() async {
|
||||||
final cachedName = await widget.profileCache?.name(widget.offer.authorPubkeyHex);
|
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();
|
final session = await widget.connection.session();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
@ -81,11 +90,18 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
||||||
unawaited(widget.profileCache
|
unawaited(widget.profileCache
|
||||||
?.setName(widget.offer.authorPubkeyHex, profile.name));
|
?.setName(widget.offer.authorPubkeyHex, profile.name));
|
||||||
}
|
}
|
||||||
|
if (profile != null && profile.picture.isNotEmpty) {
|
||||||
|
unawaited(widget.profileCache
|
||||||
|
?.setPicture(widget.offer.authorPubkeyHex, profile.picture));
|
||||||
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (profile != null && profile.name.isNotEmpty) {
|
if (profile != null && profile.name.isNotEmpty) {
|
||||||
_sellerName = profile.name;
|
_sellerName = profile.name;
|
||||||
}
|
}
|
||||||
|
if (profile != null && profile.picture.isNotEmpty) {
|
||||||
|
_sellerPicture = profile.picture;
|
||||||
|
}
|
||||||
_sellerAbout = profile?.about.isNotEmpty == true ? profile!.about : null;
|
_sellerAbout = profile?.about.isNotEmpty == true ? profile!.about : null;
|
||||||
_sellerG1 = profile?.g1.isNotEmpty == true ? profile!.g1 : null;
|
_sellerG1 = profile?.g1.isNotEmpty == true ? profile!.g1 : null;
|
||||||
_profileLoading = false;
|
_profileLoading = false;
|
||||||
|
|
@ -194,7 +210,9 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_SellerSection(
|
_SellerSection(
|
||||||
title: t.market.sharedBy,
|
title: t.market.sharedBy,
|
||||||
|
pubkey: o.authorPubkeyHex,
|
||||||
name: _sellerName ?? shortPubkey(o.authorPubkeyHex),
|
name: _sellerName ?? shortPubkey(o.authorPubkeyHex),
|
||||||
|
picture: _sellerPicture,
|
||||||
about: _sellerAbout,
|
about: _sellerAbout,
|
||||||
rating: _sellerRating,
|
rating: _sellerRating,
|
||||||
loading: _profileLoading,
|
loading: _profileLoading,
|
||||||
|
|
@ -250,6 +268,7 @@ class _Badge extends StatelessWidget {
|
||||||
class _SellerSection extends StatelessWidget {
|
class _SellerSection extends StatelessWidget {
|
||||||
const _SellerSection({
|
const _SellerSection({
|
||||||
required this.title,
|
required this.title,
|
||||||
|
required this.pubkey,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.about,
|
required this.about,
|
||||||
required this.loading,
|
required this.loading,
|
||||||
|
|
@ -257,11 +276,14 @@ class _SellerSection extends StatelessWidget {
|
||||||
required this.noProfileLabel,
|
required this.noProfileLabel,
|
||||||
required this.onCopyId,
|
required this.onCopyId,
|
||||||
required this.copyIdTooltip,
|
required this.copyIdTooltip,
|
||||||
|
this.picture,
|
||||||
this.rating,
|
this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
final String pubkey;
|
||||||
final String name;
|
final String name;
|
||||||
|
final String? picture;
|
||||||
final String? about;
|
final String? about;
|
||||||
|
|
||||||
/// Circle-weighted summary of the author's ratings; stars show only when
|
/// Circle-weighted summary of the author's ratings; stars show only when
|
||||||
|
|
@ -289,11 +311,7 @@ class _SellerSection extends StatelessWidget {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const CircleAvatar(
|
PeerAvatar(pubkey: pubkey, name: name, picture: picture, radius: 20),
|
||||||
radius: 20,
|
|
||||||
backgroundColor: seedPrimaryContainer,
|
|
||||||
child: Icon(Icons.person_outline, color: seedGreen),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
|
|
||||||
Future<void> _editAvatar() async {
|
Future<void> _editAvatar() async {
|
||||||
final result = await showAvatarPicker(context, current: _avatar);
|
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<void> _copyId() async {
|
Future<void> _copyId() async {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,23 @@ void main() {
|
||||||
await disposeTree(tester);
|
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(
|
testWidgets(
|
||||||
'renders seeds from the repository grouped under their category',
|
'renders seeds from the repository grouped under their category',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue