feat(ui): Material 3 redesign, cover-photo viewer, About screen

Apply a Material 3 seed-green palette and rounded components across the
home, inventory, quick-add, quantity picker and variety-detail screens.
The full-screen photo viewer can now set any photo as the cover (via
attachment sort order) and delete after confirmation. Lot forms and
packaging surface as choice chips.

Extract About out of Settings into its own screen (app version via
package_info_plus, website link). Add es/en strings for the new lot
types, presentation, home tagline and About. Update Seedees v2 mockups.
This commit is contained in:
vjrj 2026-07-09 11:51:59 +02:00
parent f5c36f2369
commit 42c16c0e3f
24 changed files with 1960 additions and 416 deletions

View file

@ -1,6 +1,5 @@
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';
@ -28,17 +27,37 @@ const seedScaleKinds = <QuantityKind>[
QuantityKind.bunch,
];
/// Units for a plant/seedling lot.
const plantScaleKinds = <QuantityKind>[
QuantityKind.plant,
QuantityKind.pot,
QuantityKind.tray,
];
/// The amount units offered for each lot form. Living forms are counted as
/// whole individuals (their packaging is a separate [Presentation]); [bulb] also
/// allows grams. [pot]/[tray] are intentionally not offered anymore.
List<QuantityKind> kindsForType(LotType type) => switch (type) {
LotType.seed => seedScaleKinds,
LotType.seedling => const [QuantityKind.seedling],
LotType.plant => const [QuantityKind.plant],
LotType.tree => const [QuantityKind.tree],
LotType.bulb => const [
QuantityKind.bulb,
QuantityKind.tuber,
QuantityKind.grams,
],
LotType.cutting => const [QuantityKind.cutting],
};
List<QuantityKind> kindsForType(LotType type) =>
type == LotType.seed ? seedScaleKinds : plantScaleKinds;
/// The human label for a lot form.
String lotTypeLabel(Translations t, LotType type) => switch (type) {
LotType.seed => t.lotType.seed,
LotType.seedling => t.lotType.seedling,
LotType.plant => t.lotType.plant,
LotType.tree => t.lotType.tree,
LotType.bulb => t.lotType.bulb,
LotType.cutting => t.lotType.cutting,
};
/// Seed vs plant/seedling segmented selector.
/// Living forms whose packaging ([Presentation]) is worth asking about.
bool typeHasPresentation(LotType type) =>
type == LotType.seedling || type == LotType.plant || type == LotType.tree;
/// Form selector: a wrap of choice chips, one per [LotType].
class LotTypeSelector extends StatelessWidget {
const LotTypeSelector({
required this.value,
@ -52,26 +71,65 @@ class LotTypeSelector extends StatelessWidget {
@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),
),
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final type in LotType.values)
ChoiceChip(
key: Key('lotType.${type.name}'),
selected: value == type,
onSelected: (_) => onChanged(type),
avatar: type == LotType.seed
? const SeedGlyph(SeedGlyphs.jars, size: 18)
: Icon(iconForLotType(type), size: 18),
label: Text(lotTypeLabel(t, type)),
),
],
selected: {value},
showSelectedIcon: false,
onSelectionChanged: (s) => onChanged(s.first),
);
}
}
/// Optional packaging selector for living lots. Tapping the selected chip again
/// clears it (packaging is optional).
class PresentationSelector extends StatelessWidget {
const PresentationSelector({
required this.value,
required this.onChanged,
super.key,
});
final Presentation? value;
final ValueChanged<Presentation?> onChanged;
@override
Widget build(BuildContext context) {
final t = context.t;
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final p in Presentation.values)
ChoiceChip(
key: Key('presentation.${p.name}'),
selected: value == p,
onSelected: (sel) => onChanged(sel ? p : null),
label: Text(presentationLabel(t, p)),
),
],
);
}
}
/// The human label for a packaging [Presentation].
String presentationLabel(Translations t, Presentation p) => switch (p) {
Presentation.pot => t.presentation.pot,
Presentation.tray => t.presentation.tray,
Presentation.plug => t.presentation.plug,
Presentation.bareRoot => t.presentation.bareRoot,
Presentation.rootBall => t.presentation.rootBall,
};
/// 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 {