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:
vjrj 2026-07-12 02:56:08 +02:00
parent 51d6924464
commit 6eafdfdfbb
14 changed files with 91 additions and 33 deletions

View file

@ -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,
),
],
),

View file

@ -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),
],
);
}
}

View file

@ -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<MarketOfferDetailScreen> {
String? _sellerName;
String? _sellerAbout;
String? _sellerG1;
String? _sellerPicture;
bool _profileLoading = true;
PeerRatingState? _sellerRating;
@ -66,7 +68,14 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
/// same path the chat header uses. Degrades silently when offline.
Future<void> _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<MarketOfferDetailScreen> {
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<MarketOfferDetailScreen> {
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(

View file

@ -114,7 +114,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
Future<void> _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<void> _copyId() async {