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
90
apps/app_seeds/lib/domain/species_reference_links.dart
Normal file
90
apps/app_seeds/lib/domain/species_reference_links.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/// A verified external reference for a species, built deterministically from the
|
||||
/// bundled catalog's identifiers — never hand-entered. These are read-only
|
||||
/// "learn more" pointers, distinct from the links a grower adds by hand.
|
||||
enum ReferenceSource { gbif, wikipedia, wikispecies }
|
||||
|
||||
/// One reference pointer: which source it is (for the label/icon) and the URL.
|
||||
class ReferenceLink {
|
||||
const ReferenceLink({required this.source, required this.url});
|
||||
|
||||
final ReferenceSource source;
|
||||
final String url;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ReferenceLink && other.source == source && other.url == url;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(source, url);
|
||||
|
||||
@override
|
||||
String toString() => 'ReferenceLink($source, $url)';
|
||||
}
|
||||
|
||||
/// Builds the verified reference links for a species from its bundled
|
||||
/// identifiers. Deterministic and offline — no titles are bundled and no
|
||||
/// network call is made; each URL is derived from a stable identifier:
|
||||
///
|
||||
/// * **GBIF** — the taxon page for the backbone [gbifKey].
|
||||
/// * **Wikipedia** — locale-aware via Wikidata's `Special:GoToLinkedPage`
|
||||
/// redirect, so we need only the [wikidataQid] and the user's [languageCode]:
|
||||
/// the redirect resolves to the article in that language, falling back to the
|
||||
/// Wikidata item when the language has no article. Works under RTL locales.
|
||||
/// * **Wikispecies** — the canonical article keyed by [scientificName].
|
||||
///
|
||||
/// A source is only included when its required identifier is present, so a
|
||||
/// species missing (say) a GBIF key simply yields fewer links.
|
||||
List<ReferenceLink> speciesReferenceLinks({
|
||||
required String scientificName,
|
||||
int? gbifKey,
|
||||
String? wikidataQid,
|
||||
String languageCode = 'en',
|
||||
}) {
|
||||
final links = <ReferenceLink>[];
|
||||
|
||||
if (gbifKey != null) {
|
||||
links.add(
|
||||
ReferenceLink(
|
||||
source: ReferenceSource.gbif,
|
||||
url: 'https://www.gbif.org/species/$gbifKey',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final qid = wikidataQid?.trim();
|
||||
if (qid != null && qid.isNotEmpty) {
|
||||
final lang = _wikiLang(languageCode);
|
||||
links.add(
|
||||
ReferenceLink(
|
||||
source: ReferenceSource.wikipedia,
|
||||
url:
|
||||
'https://www.wikidata.org/wiki/Special:GoToLinkedPage/${lang}wiki/$qid',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final title = scientificName.trim();
|
||||
if (title.isNotEmpty) {
|
||||
links.add(
|
||||
ReferenceLink(
|
||||
source: ReferenceSource.wikispecies,
|
||||
url: 'https://species.wikimedia.org/wiki/${_wikiTitle(title)}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
/// The bare Wikipedia language subtag: drops any region (`pt_BR` → `pt`) and
|
||||
/// falls back to English for an empty/blank locale.
|
||||
String _wikiLang(String languageCode) {
|
||||
final base = languageCode.split(RegExp('[-_]')).first.trim().toLowerCase();
|
||||
return base.isEmpty ? 'en' : base;
|
||||
}
|
||||
|
||||
/// A MediaWiki page title: spaces become underscores, then non-ASCII is
|
||||
/// percent-encoded (scientific names are Latin, but stay safe for hybrids and
|
||||
/// diacritics). `_` is left intact by [Uri.encodeFull].
|
||||
String _wikiTitle(String scientificName) =>
|
||||
Uri.encodeFull(scientificName.replaceAll(' ', '_'));
|
||||
Loading…
Add table
Add a link
Reference in a new issue