- Photo carousel dots are now tappable (jump to that photo) with a larger hit area, so you can browse all images even when swiping the small thumbnail is awkward. - The quantity unit scale is a Wrap instead of a horizontal scroll, so every unit stays visible instead of being cut off.
254 lines
6.9 KiB
Dart
254 lines
6.9 KiB
Dart
import 'package:commons_core/commons_core.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
|
|
import '../db/enums.dart';
|
|
import '../i18n/strings.g.dart';
|
|
import 'quantity_kind_l10n.dart';
|
|
import 'seed_glyph.dart';
|
|
import 'theme.dart';
|
|
|
|
/// Informal seed amounts, ascending: vibes → containers → seed/fruit forms.
|
|
/// ([QuantityKind.handful] is intentionally omitted — it overlaps with spoon.)
|
|
const seedScaleKinds = <QuantityKind>[
|
|
QuantityKind.aFew,
|
|
QuantityKind.teaspoon,
|
|
QuantityKind.spoon,
|
|
QuantityKind.packet,
|
|
QuantityKind.cup,
|
|
QuantityKind.jar,
|
|
QuantityKind.sack,
|
|
QuantityKind.cob,
|
|
QuantityKind.pod,
|
|
QuantityKind.ear,
|
|
QuantityKind.head,
|
|
QuantityKind.fruit,
|
|
QuantityKind.bulb,
|
|
QuantityKind.tuber,
|
|
QuantityKind.bunch,
|
|
];
|
|
|
|
/// Units for a plant/seedling lot.
|
|
const plantScaleKinds = <QuantityKind>[
|
|
QuantityKind.plant,
|
|
QuantityKind.pot,
|
|
QuantityKind.tray,
|
|
];
|
|
|
|
List<QuantityKind> kindsForType(LotType type) =>
|
|
type == LotType.seed ? seedScaleKinds : plantScaleKinds;
|
|
|
|
/// Seed vs plant/seedling segmented selector.
|
|
class LotTypeSelector extends StatelessWidget {
|
|
const LotTypeSelector({
|
|
required this.value,
|
|
required this.onChanged,
|
|
super.key,
|
|
});
|
|
|
|
final LotType value;
|
|
final ValueChanged<LotType> onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = context.t;
|
|
return SegmentedButton<LotType>(
|
|
segments: [
|
|
ButtonSegment(
|
|
value: LotType.seed,
|
|
label: Text(t.lotType.seed),
|
|
icon: const SeedGlyph(SeedGlyphs.jars, size: 18),
|
|
),
|
|
ButtonSegment(
|
|
value: LotType.plant,
|
|
label: Text(t.lotType.plant),
|
|
icon: const Icon(Symbols.potted_plant, size: 18),
|
|
),
|
|
],
|
|
selected: {value},
|
|
showSelectedIcon: false,
|
|
onSelectionChanged: (s) => onChanged(s.first),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A horizontal scale of large unit icons + an optional number stepper. Emits a
|
|
/// [Quantity] (or null while nothing is picked) via [onChanged].
|
|
class QuantityPicker extends StatefulWidget {
|
|
const QuantityPicker({
|
|
required this.type,
|
|
required this.onChanged,
|
|
this.value,
|
|
super.key,
|
|
});
|
|
|
|
final LotType type;
|
|
final Quantity? value;
|
|
final ValueChanged<Quantity?> onChanged;
|
|
|
|
@override
|
|
State<QuantityPicker> createState() => _QuantityPickerState();
|
|
}
|
|
|
|
class _QuantityPickerState extends State<QuantityPicker> {
|
|
QuantityKind? _kind;
|
|
final _countController = TextEditingController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_kind = widget.value?.kind;
|
|
final c = widget.value?.count;
|
|
if (c != null) _countController.text = _fmt(c);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(QuantityPicker old) {
|
|
super.didUpdateWidget(old);
|
|
// Switching seed/plant, or the owner clearing the value, drops a stale
|
|
// selection. We only update local fields here (no onChanged during build);
|
|
// the owner clears its own value alongside the type change.
|
|
if (widget.value == null && _kind != null) {
|
|
_kind = null;
|
|
_countController.clear();
|
|
} else if (widget.type != old.type &&
|
|
_kind != null &&
|
|
!kindsForType(widget.type).contains(_kind)) {
|
|
_kind = null;
|
|
_countController.clear();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_countController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _emit() {
|
|
final kind = _kind;
|
|
if (kind == null) {
|
|
widget.onChanged(null);
|
|
return;
|
|
}
|
|
final n = num.tryParse(_countController.text.trim().replaceAll(',', '.'));
|
|
widget.onChanged(Quantity(kind: kind, count: kind.countable ? n : null));
|
|
}
|
|
|
|
void _select(QuantityKind kind) {
|
|
setState(() => _kind = kind);
|
|
_emit();
|
|
}
|
|
|
|
void _bump(int delta) {
|
|
final current = int.tryParse(_countController.text.trim()) ?? 0;
|
|
final next = (current + delta).clamp(0, 9999);
|
|
_countController.text = next == 0 ? '' : '$next';
|
|
_emit();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = context.t;
|
|
final kinds = kindsForType(widget.type);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Wrap (not a horizontal scroll) so every unit stays visible.
|
|
Wrap(
|
|
spacing: 4,
|
|
runSpacing: 4,
|
|
children: [
|
|
for (final kind in kinds)
|
|
_ScaleItem(
|
|
kind: kind,
|
|
label: unitLabel(t, kind),
|
|
selected: _kind == kind,
|
|
onTap: () => _select(kind),
|
|
),
|
|
],
|
|
),
|
|
if (_kind?.countable ?? false) ...[
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
IconButton.filledTonal(
|
|
onPressed: () => _bump(-1),
|
|
icon: const Icon(Icons.remove),
|
|
),
|
|
SizedBox(
|
|
width: 64,
|
|
child: TextField(
|
|
key: const Key('quantity.count'),
|
|
controller: _countController,
|
|
textAlign: TextAlign.center,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(isDense: true),
|
|
onChanged: (_) => _emit(),
|
|
),
|
|
),
|
|
IconButton.filledTonal(
|
|
onPressed: () => _bump(1),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
unitLabel(t, _kind!),
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
String _fmt(num n) =>
|
|
n == n.roundToDouble() ? n.toInt().toString() : n.toString();
|
|
|
|
class _ScaleItem extends StatelessWidget {
|
|
const _ScaleItem({
|
|
required this.kind,
|
|
required this.label,
|
|
required this.selected,
|
|
required this.onTap,
|
|
});
|
|
|
|
final QuantityKind kind;
|
|
final String label;
|
|
final bool selected;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = selected ? seedGreenDark : Colors.black54;
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: Container(
|
|
width: 68,
|
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
|
|
decoration: BoxDecoration(
|
|
color: selected ? seedGreen.withValues(alpha: 0.15) : null,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
QuantityKindIcon(kind, size: 30, color: color),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
label,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(fontSize: 11, color: color),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|