diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart index 76203b37..0b2f0a4a 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/search_result_card.dart @@ -147,7 +147,7 @@ class _Header extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // A single record select rebuilds this header only when the session state - // or the number of ready artifacts for *this* result changes. + // or the artifacts for *this* result change. final fetch = ref.watch( metaSearchControllerProvider.select((s) { var ready = 0; @@ -155,14 +155,27 @@ class _Header extends ConsumerWidget { if (s.isMethodReady(result.url, choice)) ready++; } final busy = s.isFetching(result.url) || s.isAnyCapturing(result.url); - return (open: s.hasOpenSession, ready: ready, busy: busy); + // Any capture/preview artifact produced for this URL, in any state + // (ready, downloading, or failed) — distinct from `ready`, which only + // counts the openable ones. + final hasArtifacts = + s.capturesFor(result.url).isNotEmpty || s.isFetched(result.url); + return ( + open: s.hasOpenSession, + ready: ready, + busy: busy, + hasArtifacts: hasArtifacts, + ); }), ); - // Once the WebSocket session has closed no further fetch/capture commands - // can be issued — keep the button only while the session is open or there - // are already-fetched artifacts to (re)open via the sheet. - final showFetch = fetch.open || fetch.ready > 0; + // Keep the button reachable while the session is open (new captures + // possible), while anything is in flight, or whenever artifacts exist for + // this result. After the socket closes the sheet itself greys out every + // capture-endpoint action (see _MethodTile.enabled) — a ready artifact + // still opens locally, a failed one stays visible with its error. This + // only governs whether that sheet is reachable, not what it permits. + final showFetch = fetch.open || fetch.busy || fetch.hasArtifacts; return Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart b/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart index 2e87951b..53ff2585 100644 --- a/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart +++ b/apps/weblibre/lib/features/web_search/presentation/widgets/web_search_infobox_card.dart @@ -168,26 +168,18 @@ class WebSearchInfoboxCard extends HookConsumerWidget { } /// Attributes worth rendering in the factsheet: those with a non-empty value, - /// minus low-signal entries. In particular a lone "type"/"kind" attribute - /// whose value just restates the infobox category (e.g. `Type: Code`) adds - /// nothing the header doesn't already convey, so we drop the whole factsheet - /// in that case rather than show a one-line, oddly-centered tile. + /// minus low-signal entries. We always drop a "type"/"kind" attribute: it is + /// Brave's weakest classifier (emitted only as a fallback when no `category` + /// is available) and as a single taxonomy word (e.g. `Type: Code`) it tells + /// the user nothing the heading/image/links don't already convey. static List _meaningfulAttributes(CompactInfobox info) { - final attributes = (info.attributes ?? const []) + return (info.attributes ?? const []) .where((attr) => attr.value?.trim().isNotEmpty ?? false) + .where((attr) { + final label = attr.label.replaceAll(RegExp(r':+\s*$'), '').toLowerCase(); + return label != 'type' && label != 'kind'; + }) .toList(); - - if (attributes.length == 1) { - final only = attributes.single; - final label = only.label.replaceAll(RegExp(r':+\s*$'), '').toLowerCase(); - final value = only.value!.trim().toLowerCase(); - if ((label == 'type' || label == 'kind') && - value == info.infobox.trim().toLowerCase()) { - return const []; - } - } - - return attributes; } /// Link chips to show. These are the card's primary actionable content — the