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
|
|
@ -71,6 +71,13 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||
month: _month,
|
||||
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),
|
||||
Expanded(
|
||||
child: StreamBuilder<List<CalendarEntry>>(
|
||||
|
|
|
|||
|
|
@ -147,9 +147,22 @@ class _DetailView extends StatelessWidget {
|
|||
scientificName: detail.scientificName,
|
||||
family: detail.family ?? detail.category,
|
||||
) case final guide? when guide.hasAny) ...[
|
||||
const SizedBox(height: 16),
|
||||
_SectionTitle(t.seedSaving.title),
|
||||
_SeedSavingView(guide: guide),
|
||||
const SizedBox(height: 8),
|
||||
// Reference data (facts about the species, not the grower's own
|
||||
// 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
|
||||
when references.isNotEmpty) ...[
|
||||
|
|
@ -234,10 +247,7 @@ String _lotSubtitle(Translations t, VarietyLot lot) {
|
|||
harvestDateLabel(t, year: lot.harvestYear!, month: lot.harvestMonth)
|
||||
else
|
||||
t.detail.noYear,
|
||||
if (lot.quantity != null)
|
||||
quantityDisplay(t, lot.quantity!)
|
||||
else if (lot.abundance != null)
|
||||
abundanceLabel(t, lot.abundance!),
|
||||
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
||||
if (lot.presentation != null) presentationLabel(t, lot.presentation!),
|
||||
if (lot.offerStatus != OfferStatus.private)
|
||||
shareStatusLabel(t, lot.offerStatus),
|
||||
|
|
@ -879,6 +889,17 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
title: Text(t.cropCalendar.title),
|
||||
onExpansionChanged: (v) => _calendarOpen = v,
|
||||
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(
|
||||
label: t.cropCalendar.sow,
|
||||
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.
|
||||
String shareStatusLabel(Translations t, OfferStatus s) => switch (s) {
|
||||
OfferStatus.private => t.share.private,
|
||||
|
|
@ -1210,32 +1223,6 @@ String desiccantLabel(Translations t, DesiccantState d) => switch (d) {
|
|||
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
|
||||
/// last, never preselected — the gift is first-class, the sale a choice
|
||||
|
|
@ -1464,12 +1451,10 @@ Future<void> _showLotSheet(
|
|||
final originPlaceCtrl = TextEditingController(
|
||||
text: existing?.originPlace ?? '',
|
||||
);
|
||||
var selectedAbundance = existing?.abundance;
|
||||
var selectedPreservation = existing?.preservationFormat;
|
||||
var selectedShare = existing?.offerStatus ?? OfferStatus.private;
|
||||
var showOrigin =
|
||||
existing?.originName != null || existing?.originPlace != null;
|
||||
var showAbundance = existing?.abundance != null;
|
||||
var showShare = selectedShare != OfferStatus.private;
|
||||
final existingPrice = existing?.priceAmount;
|
||||
final priceAmountCtrl = TextEditingController(
|
||||
|
|
@ -1585,17 +1570,6 @@ Future<void> _showLotSheet(
|
|||
label: Text(t.provenance.section),
|
||||
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)
|
||||
ActionChip(
|
||||
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) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
|
|
@ -1668,17 +1621,6 @@ Future<void> _showLotSheet(
|
|||
value: selectedShare,
|
||||
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
|
||||
// paid off-app. Empty amount = "to be agreed".
|
||||
if (selectedShare == OfferStatus.sell) ...[
|
||||
|
|
@ -1786,7 +1728,6 @@ Future<void> _showLotSheet(
|
|||
presentation: selectedPresentation,
|
||||
originName: originName,
|
||||
originPlace: originPlace,
|
||||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
|
|
@ -1801,7 +1742,6 @@ Future<void> _showLotSheet(
|
|||
presentation: selectedPresentation,
|
||||
originName: originName,
|
||||
originPlace: originPlace,
|
||||
abundance: selectedAbundance,
|
||||
preservationFormat: selectedPreservation,
|
||||
offerStatus: selectedShare,
|
||||
priceAmount: priceAmount,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue