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
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue