feat(species): derived verified reference links (GBIF/Wikipedia/Wikispecies)
Add a read-only "Learn more" section on the variety detail that surfaces verified external references for the linked species, built deterministically from bundled identifiers — no titles bundled, no network, offline-first: - GBIF taxon page from the backbone key - Wikipedia via Wikidata Special:GoToLinkedPage (locale-aware, RTL-safe, falls back to the Wikidata item when a language lacks the article) - Wikispecies from the scientific name Enrich the bundled catalog (species.json v3) with gbif_key + wikidata_qid for all 14 species, cross-checked (each QID carries GBIF ID P846 = the key). Schema already had the columns; seedBundled now persists and backfills them. The grower's own manual links stay but are demoted (progressive disclosure): the section only shows when links exist and "Add link" is a low-emphasis button, since hand-adding a link is the rarer power move. Pure-Dart link builder is unit-tested; repo persistence/backfill and the detail-screen section are covered too.
This commit is contained in:
parent
ba87bf2719
commit
fde0ea6eab
14 changed files with 484 additions and 46 deletions
|
|
@ -10,6 +10,7 @@ import '../data/variety_repository.dart';
|
|||
import '../db/enums.dart';
|
||||
import '../domain/crop_calendar.dart';
|
||||
import '../domain/seed_viability.dart';
|
||||
import '../domain/species_reference_links.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../state/variety_detail_cubit.dart';
|
||||
import 'harvest_date_picker.dart';
|
||||
|
|
@ -116,29 +117,54 @@ class _DetailView extends StatelessWidget {
|
|||
_SectionTitle(t.cropCalendar.title),
|
||||
_CropCalendarView(detail: detail),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.links),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final link in detail.links)
|
||||
InputChip(
|
||||
key: Key('link.${link.id}'),
|
||||
avatar: const Icon(Icons.link, size: 18),
|
||||
label: Text(link.display),
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
onDeleted: () => cubit.removeExternalLink(link.id),
|
||||
onPressed: () => _openUrl(link.url),
|
||||
),
|
||||
ActionChip(
|
||||
key: const Key('link.add'),
|
||||
avatar: const Icon(Icons.add, size: 18),
|
||||
label: Text(t.detail.addLink),
|
||||
onPressed: () => _showAddLinkDialog(context, cubit),
|
||||
),
|
||||
],
|
||||
if (_referenceLinks(context, detail) case final references
|
||||
when references.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.reference),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final ref in references)
|
||||
ActionChip(
|
||||
key: Key('ref.${ref.source.name}'),
|
||||
avatar: const Icon(Icons.open_in_new, size: 18),
|
||||
label: Text(_referenceLabel(t, ref.source)),
|
||||
onPressed: () => _openUrl(ref.url),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
// The grower's own links: a rarer power move, so kept subtle — the
|
||||
// section only appears once there are links, and adding one lives
|
||||
// behind a low-emphasis button rather than a prominent chip.
|
||||
if (detail.links.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.detail.links),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final link in detail.links)
|
||||
InputChip(
|
||||
key: Key('link.${link.id}'),
|
||||
avatar: const Icon(Icons.link, size: 18),
|
||||
label: Text(link.display),
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
onDeleted: () => cubit.removeExternalLink(link.id),
|
||||
onPressed: () => _openUrl(link.url),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton.icon(
|
||||
key: const Key('link.add'),
|
||||
onPressed: () => _showAddLinkDialog(context, cubit),
|
||||
icon: const Icon(Icons.add_link, size: 18),
|
||||
label: Text(t.detail.addLink),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
|
|
@ -1497,6 +1523,30 @@ Future<void> _showAddNameDialog(
|
|||
);
|
||||
}
|
||||
|
||||
/// Verified reference links for the variety's linked species, localized to the
|
||||
/// UI language (drives the locale-aware Wikipedia link). Empty when no species
|
||||
/// is linked or the catalog entry carries no identifiers.
|
||||
List<ReferenceLink> _referenceLinks(
|
||||
BuildContext context,
|
||||
VarietyDetail detail,
|
||||
) {
|
||||
final name = detail.scientificName;
|
||||
if (name == null) return const [];
|
||||
return speciesReferenceLinks(
|
||||
scientificName: name,
|
||||
gbifKey: detail.gbifKey,
|
||||
wikidataQid: detail.wikidataQid,
|
||||
languageCode: Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
}
|
||||
|
||||
String _referenceLabel(Translations t, ReferenceSource source) =>
|
||||
switch (source) {
|
||||
ReferenceSource.gbif => t.detail.refGbif,
|
||||
ReferenceSource.wikipedia => t.detail.refWikipedia,
|
||||
ReferenceSource.wikispecies => t.detail.refWikispecies,
|
||||
};
|
||||
|
||||
Future<void> _openUrl(String url) async {
|
||||
final normalized = url.contains('://') ? url : 'https://$url';
|
||||
final uri = Uri.tryParse(normalized);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue