polish(detail): TODO review — calendar intro, drop abundance, collapse seed-saving
From the owner's TODO.org review of the shipped features: - Crop calendar now says the months are YOUR notes: a hint line in the editor (cropCalendar.editorHint) and a caption under the 'this month' screen (calendar.selfNote) — it read as if the app knew when to sow. - Dropped the 'how much I have' (abundance) input + display: it competed with the lot quantity as a second 'how much'. Removed from the lot sheet and the lot summary; the DB column + backup/CSV/catalog plumbing stay dormant (no destructive migration). offer_status carries 'do I share'. - Seed-saving guidance moved into a collapsed expander (detail.seedSaving) — reference facts about the species shouldn't sit open among the grower's own records; opt-in depth, per progressive disclosure. - Condition checks already live behind the collapsed 'advanced' expander (seed lots only) — left as the discreet power-user surface. Tests: dropped the obsolete abundance→share nudge test; the seed-saving section test now expands before asserting. analyze clean; suites green.
This commit is contained in:
parent
00db9d4b6a
commit
6898e385e8
13 changed files with 73 additions and 124 deletions
|
|
@ -30,6 +30,7 @@
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Esti mes",
|
"title": "Esti mes",
|
||||||
"filterChip": "Esti mes",
|
"filterChip": "Esti mes",
|
||||||
|
"selfNote": "Lo qu'anotasti nes tos variedaes.",
|
||||||
"nothing": "Nada anotao pa {month}."
|
"nothing": "Nada anotao pa {month}."
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|
@ -342,6 +343,7 @@
|
||||||
"flowering": "Floriamientu",
|
"flowering": "Floriamientu",
|
||||||
"fruiting": "Fructificación",
|
"fruiting": "Fructificación",
|
||||||
"seedHarvest": "Coyecha de simiente",
|
"seedHarvest": "Coyecha de simiente",
|
||||||
|
"editorHint": "Anota tú los meses típicos d'esta variedá na to zona — son notes tuyes.",
|
||||||
"unset": "—"
|
"unset": "—"
|
||||||
},
|
},
|
||||||
"needsReproduction": {
|
"needsReproduction": {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "This month",
|
"title": "This month",
|
||||||
"filterChip": "This month",
|
"filterChip": "This month",
|
||||||
|
"selfNote": "What you've noted in your varieties.",
|
||||||
"nothing": "Nothing noted for {month}."
|
"nothing": "Nothing noted for {month}."
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|
@ -343,6 +344,7 @@
|
||||||
"flowering": "Flowering",
|
"flowering": "Flowering",
|
||||||
"fruiting": "Fruiting",
|
"fruiting": "Fruiting",
|
||||||
"seedHarvest": "Seed harvest",
|
"seedHarvest": "Seed harvest",
|
||||||
|
"editorHint": "Note the months typical for this variety in your area — these are your own notes.",
|
||||||
"unset": "—"
|
"unset": "—"
|
||||||
},
|
},
|
||||||
"needsReproduction": {
|
"needsReproduction": {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Este mes",
|
"title": "Este mes",
|
||||||
"filterChip": "Este mes",
|
"filterChip": "Este mes",
|
||||||
|
"selfNote": "Lo que has anotado en tus variedades.",
|
||||||
"nothing": "Nada anotado para {month}."
|
"nothing": "Nada anotado para {month}."
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|
@ -342,6 +343,7 @@
|
||||||
"flowering": "Floración",
|
"flowering": "Floración",
|
||||||
"fruiting": "Fructificación",
|
"fruiting": "Fructificación",
|
||||||
"seedHarvest": "Cosecha de semilla",
|
"seedHarvest": "Cosecha de semilla",
|
||||||
|
"editorHint": "Anota tú los meses típicos de esta variedad en tu zona — son tus propias notas.",
|
||||||
"unset": "—"
|
"unset": "—"
|
||||||
},
|
},
|
||||||
"needsReproduction": {
|
"needsReproduction": {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Este mês",
|
"title": "Este mês",
|
||||||
"filterChip": "Este mês",
|
"filterChip": "Este mês",
|
||||||
|
"selfNote": "O que anotaste nas tuas variedades.",
|
||||||
"nothing": "Nada anotado para {month}."
|
"nothing": "Nada anotado para {month}."
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|
@ -339,6 +340,7 @@
|
||||||
"flowering": "Floração",
|
"flowering": "Floração",
|
||||||
"fruiting": "Frutificação",
|
"fruiting": "Frutificação",
|
||||||
"seedHarvest": "Colheita de semente",
|
"seedHarvest": "Colheita de semente",
|
||||||
|
"editorHint": "Anota tu os meses típicos desta variedade na tua zona — são as tuas notas.",
|
||||||
"unset": "—"
|
"unset": "—"
|
||||||
},
|
},
|
||||||
"needsReproduction": {
|
"needsReproduction": {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 4
|
/// Locales: 4
|
||||||
/// Strings: 2008 (502 per locale)
|
/// Strings: 2016 (504 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-11 at 11:29 UTC
|
/// Built on 2026-07-11 at 12:07 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ class _Translations$calendar$ast extends Translations$calendar$en {
|
||||||
// Translations
|
// Translations
|
||||||
@override String get title => 'Esti mes';
|
@override String get title => 'Esti mes';
|
||||||
@override String get filterChip => 'Esti mes';
|
@override String get filterChip => 'Esti mes';
|
||||||
|
@override String get selfNote => 'Lo qu\'anotasti nes tos variedaes.';
|
||||||
@override String nothing({required Object month}) => 'Nada anotao pa ${month}.';
|
@override String nothing({required Object month}) => 'Nada anotao pa ${month}.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -602,6 +603,7 @@ class _Translations$cropCalendar$ast extends Translations$cropCalendar$en {
|
||||||
@override String get flowering => 'Floriamientu';
|
@override String get flowering => 'Floriamientu';
|
||||||
@override String get fruiting => 'Fructificación';
|
@override String get fruiting => 'Fructificación';
|
||||||
@override String get seedHarvest => 'Coyecha de simiente';
|
@override String get seedHarvest => 'Coyecha de simiente';
|
||||||
|
@override String get editorHint => 'Anota tú los meses típicos d\'esta variedá na to zona — son notes tuyes.';
|
||||||
@override String get unset => '—';
|
@override String get unset => '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1320,6 +1322,7 @@ extension on TranslationsAst {
|
||||||
'seedSaving.sourcePrefix' => 'Fonte',
|
'seedSaving.sourcePrefix' => 'Fonte',
|
||||||
'calendar.title' => 'Esti mes',
|
'calendar.title' => 'Esti mes',
|
||||||
'calendar.filterChip' => 'Esti mes',
|
'calendar.filterChip' => 'Esti mes',
|
||||||
|
'calendar.selfNote' => 'Lo qu\'anotasti nes tos variedaes.',
|
||||||
'calendar.nothing' => ({required Object month}) => 'Nada anotao pa ${month}.',
|
'calendar.nothing' => ({required Object month}) => 'Nada anotao pa ${month}.',
|
||||||
'app.title' => 'Tanemaki',
|
'app.title' => 'Tanemaki',
|
||||||
'bootstrap.failed' => 'Tanemaki nun pudo aniciar',
|
'bootstrap.failed' => 'Tanemaki nun pudo aniciar',
|
||||||
|
|
@ -1566,6 +1569,7 @@ extension on TranslationsAst {
|
||||||
'cropCalendar.flowering' => 'Floriamientu',
|
'cropCalendar.flowering' => 'Floriamientu',
|
||||||
'cropCalendar.fruiting' => 'Fructificación',
|
'cropCalendar.fruiting' => 'Fructificación',
|
||||||
'cropCalendar.seedHarvest' => 'Coyecha de simiente',
|
'cropCalendar.seedHarvest' => 'Coyecha de simiente',
|
||||||
|
'cropCalendar.editorHint' => 'Anota tú los meses típicos d\'esta variedá na to zona — son notes tuyes.',
|
||||||
'cropCalendar.unset' => '—',
|
'cropCalendar.unset' => '—',
|
||||||
'needsReproduction.label' => 'Reproducir esta temporada',
|
'needsReproduction.label' => 'Reproducir esta temporada',
|
||||||
'needsReproduction.hint' => 'Cultívala enantes de que s\'acabe la simiente',
|
'needsReproduction.hint' => 'Cultívala enantes de que s\'acabe la simiente',
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,9 @@ class Translations$calendar$en {
|
||||||
/// en: 'This month'
|
/// en: 'This month'
|
||||||
String get filterChip => 'This month';
|
String get filterChip => 'This month';
|
||||||
|
|
||||||
|
/// en: 'What you've noted in your varieties.'
|
||||||
|
String get selfNote => 'What you\'ve noted in your varieties.';
|
||||||
|
|
||||||
/// en: 'Nothing noted for {month}.'
|
/// en: 'Nothing noted for {month}.'
|
||||||
String nothing({required Object month}) => 'Nothing noted for ${month}.';
|
String nothing({required Object month}) => 'Nothing noted for ${month}.';
|
||||||
}
|
}
|
||||||
|
|
@ -1113,6 +1116,9 @@ class Translations$cropCalendar$en {
|
||||||
/// en: 'Seed harvest'
|
/// en: 'Seed harvest'
|
||||||
String get seedHarvest => 'Seed harvest';
|
String get seedHarvest => 'Seed harvest';
|
||||||
|
|
||||||
|
/// en: 'Note the months typical for this variety in your area — these are your own notes.'
|
||||||
|
String get editorHint => 'Note the months typical for this variety in your area — these are your own notes.';
|
||||||
|
|
||||||
/// en: '—'
|
/// en: '—'
|
||||||
String get unset => '—';
|
String get unset => '—';
|
||||||
}
|
}
|
||||||
|
|
@ -2311,6 +2317,7 @@ extension on Translations {
|
||||||
'seedSaving.sourcePrefix' => 'Source',
|
'seedSaving.sourcePrefix' => 'Source',
|
||||||
'calendar.title' => 'This month',
|
'calendar.title' => 'This month',
|
||||||
'calendar.filterChip' => 'This month',
|
'calendar.filterChip' => 'This month',
|
||||||
|
'calendar.selfNote' => 'What you\'ve noted in your varieties.',
|
||||||
'calendar.nothing' => ({required Object month}) => 'Nothing noted for ${month}.',
|
'calendar.nothing' => ({required Object month}) => 'Nothing noted for ${month}.',
|
||||||
'app.title' => 'Tanemaki',
|
'app.title' => 'Tanemaki',
|
||||||
'bootstrap.failed' => 'Tanemaki couldn\'t start',
|
'bootstrap.failed' => 'Tanemaki couldn\'t start',
|
||||||
|
|
@ -2558,6 +2565,7 @@ extension on Translations {
|
||||||
'cropCalendar.flowering' => 'Flowering',
|
'cropCalendar.flowering' => 'Flowering',
|
||||||
'cropCalendar.fruiting' => 'Fruiting',
|
'cropCalendar.fruiting' => 'Fruiting',
|
||||||
'cropCalendar.seedHarvest' => 'Seed harvest',
|
'cropCalendar.seedHarvest' => 'Seed harvest',
|
||||||
|
'cropCalendar.editorHint' => 'Note the months typical for this variety in your area — these are your own notes.',
|
||||||
'cropCalendar.unset' => '—',
|
'cropCalendar.unset' => '—',
|
||||||
'needsReproduction.label' => 'To regrow this season',
|
'needsReproduction.label' => 'To regrow this season',
|
||||||
'needsReproduction.hint' => 'Grow it out before the seed runs out',
|
'needsReproduction.hint' => 'Grow it out before the seed runs out',
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ class _Translations$calendar$es extends Translations$calendar$en {
|
||||||
// Translations
|
// Translations
|
||||||
@override String get title => 'Este mes';
|
@override String get title => 'Este mes';
|
||||||
@override String get filterChip => 'Este mes';
|
@override String get filterChip => 'Este mes';
|
||||||
|
@override String get selfNote => 'Lo que has anotado en tus variedades.';
|
||||||
@override String nothing({required Object month}) => 'Nada anotado para ${month}.';
|
@override String nothing({required Object month}) => 'Nada anotado para ${month}.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -602,6 +603,7 @@ class _Translations$cropCalendar$es extends Translations$cropCalendar$en {
|
||||||
@override String get flowering => 'Floración';
|
@override String get flowering => 'Floración';
|
||||||
@override String get fruiting => 'Fructificación';
|
@override String get fruiting => 'Fructificación';
|
||||||
@override String get seedHarvest => 'Cosecha de semilla';
|
@override String get seedHarvest => 'Cosecha de semilla';
|
||||||
|
@override String get editorHint => 'Anota tú los meses típicos de esta variedad en tu zona — son tus propias notas.';
|
||||||
@override String get unset => '—';
|
@override String get unset => '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1322,6 +1324,7 @@ extension on TranslationsEs {
|
||||||
'seedSaving.sourcePrefix' => 'Fuente',
|
'seedSaving.sourcePrefix' => 'Fuente',
|
||||||
'calendar.title' => 'Este mes',
|
'calendar.title' => 'Este mes',
|
||||||
'calendar.filterChip' => 'Este mes',
|
'calendar.filterChip' => 'Este mes',
|
||||||
|
'calendar.selfNote' => 'Lo que has anotado en tus variedades.',
|
||||||
'calendar.nothing' => ({required Object month}) => 'Nada anotado para ${month}.',
|
'calendar.nothing' => ({required Object month}) => 'Nada anotado para ${month}.',
|
||||||
'app.title' => 'Tanemaki',
|
'app.title' => 'Tanemaki',
|
||||||
'bootstrap.failed' => 'Tanemaki no pudo arrancar',
|
'bootstrap.failed' => 'Tanemaki no pudo arrancar',
|
||||||
|
|
@ -1568,6 +1571,7 @@ extension on TranslationsEs {
|
||||||
'cropCalendar.flowering' => 'Floración',
|
'cropCalendar.flowering' => 'Floración',
|
||||||
'cropCalendar.fruiting' => 'Fructificación',
|
'cropCalendar.fruiting' => 'Fructificación',
|
||||||
'cropCalendar.seedHarvest' => 'Cosecha de semilla',
|
'cropCalendar.seedHarvest' => 'Cosecha de semilla',
|
||||||
|
'cropCalendar.editorHint' => 'Anota tú los meses típicos de esta variedad en tu zona — son tus propias notas.',
|
||||||
'cropCalendar.unset' => '—',
|
'cropCalendar.unset' => '—',
|
||||||
'needsReproduction.label' => 'Reproducir esta temporada',
|
'needsReproduction.label' => 'Reproducir esta temporada',
|
||||||
'needsReproduction.hint' => 'Cultívala antes de que se acabe la semilla',
|
'needsReproduction.hint' => 'Cultívala antes de que se acabe la semilla',
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ class _Translations$calendar$pt extends Translations$calendar$en {
|
||||||
// Translations
|
// Translations
|
||||||
@override String get title => 'Este mês';
|
@override String get title => 'Este mês';
|
||||||
@override String get filterChip => 'Este mês';
|
@override String get filterChip => 'Este mês';
|
||||||
|
@override String get selfNote => 'O que anotaste nas tuas variedades.';
|
||||||
@override String nothing({required Object month}) => 'Nada anotado para ${month}.';
|
@override String nothing({required Object month}) => 'Nada anotado para ${month}.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -599,6 +600,7 @@ class _Translations$cropCalendar$pt extends Translations$cropCalendar$en {
|
||||||
@override String get flowering => 'Floração';
|
@override String get flowering => 'Floração';
|
||||||
@override String get fruiting => 'Frutificação';
|
@override String get fruiting => 'Frutificação';
|
||||||
@override String get seedHarvest => 'Colheita de semente';
|
@override String get seedHarvest => 'Colheita de semente';
|
||||||
|
@override String get editorHint => 'Anota tu os meses típicos desta variedade na tua zona — são as tuas notas.';
|
||||||
@override String get unset => '—';
|
@override String get unset => '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1319,6 +1321,7 @@ extension on TranslationsPt {
|
||||||
'seedSaving.sourcePrefix' => 'Fonte',
|
'seedSaving.sourcePrefix' => 'Fonte',
|
||||||
'calendar.title' => 'Este mês',
|
'calendar.title' => 'Este mês',
|
||||||
'calendar.filterChip' => 'Este mês',
|
'calendar.filterChip' => 'Este mês',
|
||||||
|
'calendar.selfNote' => 'O que anotaste nas tuas variedades.',
|
||||||
'calendar.nothing' => ({required Object month}) => 'Nada anotado para ${month}.',
|
'calendar.nothing' => ({required Object month}) => 'Nada anotado para ${month}.',
|
||||||
'app.title' => 'Tanemaki',
|
'app.title' => 'Tanemaki',
|
||||||
'bootstrap.failed' => 'O Tanemaki não conseguiu iniciar',
|
'bootstrap.failed' => 'O Tanemaki não conseguiu iniciar',
|
||||||
|
|
@ -1562,6 +1565,7 @@ extension on TranslationsPt {
|
||||||
'cropCalendar.flowering' => 'Floração',
|
'cropCalendar.flowering' => 'Floração',
|
||||||
'cropCalendar.fruiting' => 'Frutificação',
|
'cropCalendar.fruiting' => 'Frutificação',
|
||||||
'cropCalendar.seedHarvest' => 'Colheita de semente',
|
'cropCalendar.seedHarvest' => 'Colheita de semente',
|
||||||
|
'cropCalendar.editorHint' => 'Anota tu os meses típicos desta variedade na tua zona — são as tuas notas.',
|
||||||
'cropCalendar.unset' => '—',
|
'cropCalendar.unset' => '—',
|
||||||
'needsReproduction.label' => 'Reproduzir nesta época',
|
'needsReproduction.label' => 'Reproduzir nesta época',
|
||||||
'needsReproduction.hint' => 'Cultiva-a antes que a semente acabe',
|
'needsReproduction.hint' => 'Cultiva-a antes que a semente acabe',
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||||
month: _month,
|
month: _month,
|
||||||
onSelect: (m) => setState(() => _month = m),
|
onSelect: (m) => setState(() => _month = m),
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 8),
|
||||||
|
child: Text(
|
||||||
|
t.calendar.selfNote,
|
||||||
|
style: const TextStyle(color: seedMuted, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<List<CalendarEntry>>(
|
child: StreamBuilder<List<CalendarEntry>>(
|
||||||
|
|
|
||||||
|
|
@ -147,9 +147,22 @@ class _DetailView extends StatelessWidget {
|
||||||
scientificName: detail.scientificName,
|
scientificName: detail.scientificName,
|
||||||
family: detail.family ?? detail.category,
|
family: detail.family ?? detail.category,
|
||||||
) case final guide? when guide.hasAny) ...[
|
) case final guide? when guide.hasAny) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 8),
|
||||||
_SectionTitle(t.seedSaving.title),
|
// Reference data (facts about the species, not the grower's own
|
||||||
_SeedSavingView(guide: guide),
|
// records) — collapsed by default so it's opt-in depth, not noise.
|
||||||
|
ExpansionTile(
|
||||||
|
key: const Key('detail.seedSaving'),
|
||||||
|
tilePadding: EdgeInsets.zero,
|
||||||
|
childrenPadding: const EdgeInsets.only(bottom: 8),
|
||||||
|
leading: const Icon(Icons.spa_outlined, color: seedGreen),
|
||||||
|
title: Text(t.seedSaving.title),
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: _SeedSavingView(guide: guide),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
if (_referenceLinks(context, detail) case final references
|
if (_referenceLinks(context, detail) case final references
|
||||||
when references.isNotEmpty) ...[
|
when references.isNotEmpty) ...[
|
||||||
|
|
@ -234,10 +247,7 @@ String _lotSubtitle(Translations t, VarietyLot lot) {
|
||||||
harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
|
harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
|
||||||
else
|
else
|
||||||
t.detail.noYear,
|
t.detail.noYear,
|
||||||
if (lot.quantity != null)
|
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
||||||
quantityDisplay(t, lot.quantity!)
|
|
||||||
else if (lot.abundance != null)
|
|
||||||
abundanceLabel(t, lot.abundance!),
|
|
||||||
if (lot.presentation != null) presentationLabel(t, lot.presentation!),
|
if (lot.presentation != null) presentationLabel(t, lot.presentation!),
|
||||||
if (lot.offerStatus != OfferStatus.private)
|
if (lot.offerStatus != OfferStatus.private)
|
||||||
shareStatusLabel(t, lot.offerStatus),
|
shareStatusLabel(t, lot.offerStatus),
|
||||||
|
|
@ -879,6 +889,17 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
||||||
title: Text(t.cropCalendar.title),
|
title: Text(t.cropCalendar.title),
|
||||||
onExpansionChanged: (v) => _calendarOpen = v,
|
onExpansionChanged: (v) => _calendarOpen = v,
|
||||||
children: [
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Text(
|
||||||
|
t.cropCalendar.editorHint,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: seedMuted, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
_MonthMultiSelect(
|
_MonthMultiSelect(
|
||||||
label: t.cropCalendar.sow,
|
label: t.cropCalendar.sow,
|
||||||
mask: _sowMonths,
|
mask: _sowMonths,
|
||||||
|
|
@ -1176,14 +1197,6 @@ class _DifficultyChip extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Human label for a coarse-abundance level.
|
|
||||||
String abundanceLabel(Translations t, Abundance a) => switch (a) {
|
|
||||||
Abundance.plentyToShare => t.abundance.plentyToShare,
|
|
||||||
Abundance.enoughToShare => t.abundance.enoughToShare,
|
|
||||||
Abundance.enoughForMe => t.abundance.enoughForMe,
|
|
||||||
Abundance.runningLow => t.abundance.runningLow,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Human label for how (whether) a lot is offered to others.
|
/// Human label for how (whether) a lot is offered to others.
|
||||||
String shareStatusLabel(Translations t, OfferStatus s) => switch (s) {
|
String shareStatusLabel(Translations t, OfferStatus s) => switch (s) {
|
||||||
OfferStatus.private => t.share.private,
|
OfferStatus.private => t.share.private,
|
||||||
|
|
@ -1210,32 +1223,6 @@ String desiccantLabel(Translations t, DesiccantState d) => switch (d) {
|
||||||
DesiccantState.fresh => t.desiccant.fresh,
|
DesiccantState.fresh => t.desiccant.fresh,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Single-select chips for the coarse-abundance level; tapping the current one
|
|
||||||
/// clears it (null).
|
|
||||||
class _AbundanceSelector extends StatelessWidget {
|
|
||||||
const _AbundanceSelector({required this.value, required this.onChanged});
|
|
||||||
|
|
||||||
final Abundance? value;
|
|
||||||
final ValueChanged<Abundance?> onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final t = context.t;
|
|
||||||
return Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 4,
|
|
||||||
children: [
|
|
||||||
for (final a in Abundance.values)
|
|
||||||
ChoiceChip(
|
|
||||||
key: Key('abundance.${a.name}'),
|
|
||||||
label: Text(abundanceLabel(t, a)),
|
|
||||||
selected: value == a,
|
|
||||||
onSelected: (sel) => onChanged(sel ? a : null),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Single-select chips for the share terms. Gift and swap come first and sale
|
/// Single-select chips for the share terms. Gift and swap come first and sale
|
||||||
/// last, never preselected — the gift is first-class, the sale a choice
|
/// last, never preselected — the gift is first-class, the sale a choice
|
||||||
|
|
@ -1464,12 +1451,10 @@ Future<void> _showLotSheet(
|
||||||
final originPlaceCtrl = TextEditingController(
|
final originPlaceCtrl = TextEditingController(
|
||||||
text: existing?.originPlace ?? '',
|
text: existing?.originPlace ?? '',
|
||||||
);
|
);
|
||||||
var selectedAbundance = existing?.abundance;
|
|
||||||
var selectedPreservation = existing?.preservationFormat;
|
var selectedPreservation = existing?.preservationFormat;
|
||||||
var selectedShare = existing?.offerStatus ?? OfferStatus.private;
|
var selectedShare = existing?.offerStatus ?? OfferStatus.private;
|
||||||
var showOrigin =
|
var showOrigin =
|
||||||
existing?.originName != null || existing?.originPlace != null;
|
existing?.originName != null || existing?.originPlace != null;
|
||||||
var showAbundance = existing?.abundance != null;
|
|
||||||
var showShare = selectedShare != OfferStatus.private;
|
var showShare = selectedShare != OfferStatus.private;
|
||||||
final existingPrice = existing?.priceAmount;
|
final existingPrice = existing?.priceAmount;
|
||||||
final priceAmountCtrl = TextEditingController(
|
final priceAmountCtrl = TextEditingController(
|
||||||
|
|
@ -1585,17 +1570,6 @@ Future<void> _showLotSheet(
|
||||||
label: Text(t.provenance.section),
|
label: Text(t.provenance.section),
|
||||||
onPressed: () => setState(() => showOrigin = true),
|
onPressed: () => setState(() => showOrigin = true),
|
||||||
),
|
),
|
||||||
if (!showAbundance)
|
|
||||||
ActionChip(
|
|
||||||
key: const Key('lot.addAbundance'),
|
|
||||||
avatar: const Icon(
|
|
||||||
Icons.inventory_2_outlined,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
label: Text(t.abundance.add),
|
|
||||||
onPressed: () =>
|
|
||||||
setState(() => showAbundance = true),
|
|
||||||
),
|
|
||||||
if (!showShare)
|
if (!showShare)
|
||||||
ActionChip(
|
ActionChip(
|
||||||
key: const Key('lot.addShare'),
|
key: const Key('lot.addShare'),
|
||||||
|
|
@ -1636,27 +1610,6 @@ Future<void> _showLotSheet(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (showAbundance) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
t.abundance.title,
|
|
||||||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_AbundanceSelector(
|
|
||||||
value: selectedAbundance,
|
|
||||||
onChanged: (a) => setState(() {
|
|
||||||
selectedAbundance = a;
|
|
||||||
// The bridge from "how much" to "do you share it":
|
|
||||||
// declaring plenty reveals the sharing choice. A
|
|
||||||
// nudge, never a change of the choice itself.
|
|
||||||
if (a == Abundance.plentyToShare ||
|
|
||||||
a == Abundance.enoughToShare) {
|
|
||||||
showShare = true;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (showShare) ...[
|
if (showShare) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -1668,17 +1621,6 @@ Future<void> _showLotSheet(
|
||||||
value: selectedShare,
|
value: selectedShare,
|
||||||
onChanged: (s) => setState(() => selectedShare = s),
|
onChanged: (s) => setState(() => selectedShare = s),
|
||||||
),
|
),
|
||||||
if (selectedShare == OfferStatus.private &&
|
|
||||||
(selectedAbundance == Abundance.plentyToShare ||
|
|
||||||
selectedAbundance == Abundance.enoughToShare))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 6),
|
|
||||||
child: Text(
|
|
||||||
t.share.nudge,
|
|
||||||
key: const Key('share.nudge'),
|
|
||||||
style: Theme.of(sheetContext).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Price only when selling — informational, agreed and
|
// Price only when selling — informational, agreed and
|
||||||
// paid off-app. Empty amount = "to be agreed".
|
// paid off-app. Empty amount = "to be agreed".
|
||||||
if (selectedShare == OfferStatus.sell) ...[
|
if (selectedShare == OfferStatus.sell) ...[
|
||||||
|
|
@ -1786,7 +1728,6 @@ Future<void> _showLotSheet(
|
||||||
presentation: selectedPresentation,
|
presentation: selectedPresentation,
|
||||||
originName: originName,
|
originName: originName,
|
||||||
originPlace: originPlace,
|
originPlace: originPlace,
|
||||||
abundance: selectedAbundance,
|
|
||||||
preservationFormat: selectedPreservation,
|
preservationFormat: selectedPreservation,
|
||||||
offerStatus: selectedShare,
|
offerStatus: selectedShare,
|
||||||
priceAmount: priceAmount,
|
priceAmount: priceAmount,
|
||||||
|
|
@ -1801,7 +1742,6 @@ Future<void> _showLotSheet(
|
||||||
presentation: selectedPresentation,
|
presentation: selectedPresentation,
|
||||||
originName: originName,
|
originName: originName,
|
||||||
originPlace: originPlace,
|
originPlace: originPlace,
|
||||||
abundance: selectedAbundance,
|
|
||||||
preservationFormat: selectedPreservation,
|
preservationFormat: selectedPreservation,
|
||||||
offerStatus: selectedShare,
|
offerStatus: selectedShare,
|
||||||
priceAmount: priceAmount,
|
priceAmount: priceAmount,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:tane/data/seed_saving_catalog.dart';
|
import 'package:tane/data/seed_saving_catalog.dart';
|
||||||
import 'package:tane/db/database.dart';
|
import 'package:tane/db/database.dart';
|
||||||
|
|
@ -36,7 +37,14 @@ void main() {
|
||||||
testWidgets('shows the seed-saving section for a known family', (tester) async {
|
testWidgets('shows the seed-saving section for a known family', (tester) async {
|
||||||
await pumpDetailFor(tester, 'Solanaceae');
|
await pumpDetailFor(tester, 'Solanaceae');
|
||||||
|
|
||||||
|
// The section is a collapsed expander — its title shows, its body doesn't
|
||||||
|
// until opened.
|
||||||
expect(find.text('Saving its seed'), findsOneWidget);
|
expect(find.text('Saving its seed'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Self-pollinating'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('detail.seedSaving')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// The pollination line is a Text.rich (label + value), so match the run.
|
// The pollination line is a Text.rich (label + value), so match the run.
|
||||||
expect(find.textContaining('Self-pollinating'), findsOneWidget);
|
expect(find.textContaining('Self-pollinating'), findsOneWidget);
|
||||||
expect(find.text('Easy'), findsOneWidget); // the difficulty pill
|
expect(find.text('Easy'), findsOneWidget); // the difficulty pill
|
||||||
|
|
|
||||||
|
|
@ -47,40 +47,6 @@ void main() {
|
||||||
await disposeTree(tester);
|
await disposeTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('declaring plenty reveals the share choice with a nudge', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
final repo = newTestRepository(db);
|
|
||||||
final id = await repo.addQuickVariety(label: 'Maize');
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
wrapDetail(
|
|
||||||
repository: repo,
|
|
||||||
varietyId: id,
|
|
||||||
species: newTestSpeciesRepository(db),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('detail.addLot')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Reveal abundance and declare plenty.
|
|
||||||
await tester.ensureVisible(find.byKey(const Key('lot.addAbundance')));
|
|
||||||
await tester.tap(find.byKey(const Key('lot.addAbundance')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.ensureVisible(
|
|
||||||
find.byKey(const Key('abundance.plentyToShare')),
|
|
||||||
);
|
|
||||||
await tester.tap(find.byKey(const Key('abundance.plentyToShare')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// The share section reveals itself, still private, with a soft nudge.
|
|
||||||
expect(find.byKey(const Key('share.private')), findsOneWidget);
|
|
||||||
expect(find.byKey(const Key('share.nudge')), findsOneWidget);
|
|
||||||
await disposeTree(tester);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('the sharing filter narrows the list to what I share', (
|
testWidgets('the sharing filter narrows the list to what I share', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue