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

@ -6,9 +6,10 @@ import 'app_drawer.dart';
import 'seed_glyph.dart';
import 'theme.dart';
/// The main menu (mockup 02): a sprout logo over a faint seed-glyph watermark,
/// with the big "open market" (Block 2, coming soon) and "your inventory"
/// destinations. The hamburger opens [AppDrawer].
/// The main menu (redesign screen 00): a sprout logo in a soft green disc over a
/// faint seed-glyph watermark, with "Your inventory" as the primary green call
/// to action and "Open market" (Block 2) as a disabled outlined card. The
/// hamburger opens [AppDrawer].
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@ -25,33 +26,61 @@ class HomeScreen extends StatelessWidget {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440),
child: Padding(
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.asset(
'assets/logo.png',
height: 140,
fit: BoxFit.contain,
),
const SizedBox(height: 40),
_MenuButton(
icon: const SeedGlyph(
SeedGlyphs.share,
size: 26,
color: Colors.white,
Center(
child: Container(
width: 128,
height: 128,
decoration: const BoxDecoration(
color: Color(0xFFDDF3CE),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Image.asset(
'assets/logo.png',
height: 84,
fit: BoxFit.contain,
),
),
label: t.home.openMarket,
tag: t.common.comingSoon,
onTap: null,
),
const SizedBox(height: 10),
Text(
t.app.title,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF1A2A13),
fontSize: 26,
fontWeight: FontWeight.w500,
letterSpacing: -0.3,
),
),
const SizedBox(height: 2),
Text(
t.home.tagline,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF55624A),
fontSize: 14,
),
),
const SizedBox(height: 34),
_PrimaryMenuCard(
key: const Key('home.inventory'),
icon: Icons.inventory_2_outlined,
label: t.home.yourInventory,
subtitle: t.home.yourInventorySubtitle,
onTap: () => context.go('/inventory'),
),
const SizedBox(height: 16),
_MenuButton(
key: const Key('home.inventory'),
icon: const Icon(Icons.list, color: Colors.white),
label: t.home.yourInventory,
onTap: () => context.go('/inventory'),
_OutlinedMenuCard(
icon: Icons.storefront_outlined,
label: t.home.openMarket,
subtitle: t.home.openMarketSubtitle,
tag: t.common.comingSoon,
),
],
),
@ -64,50 +93,49 @@ class HomeScreen extends StatelessWidget {
}
}
class _MenuButton extends StatelessWidget {
const _MenuButton({
/// The primary green call-to-action: white icon in a translucent disc, a title
/// and a subtitle, with a soft green shadow.
class _PrimaryMenuCard extends StatelessWidget {
const _PrimaryMenuCard({
required this.icon,
required this.label,
this.onTap,
this.tag,
required this.subtitle,
required this.onTap,
super.key,
});
final Widget icon;
final IconData icon;
final String label;
final VoidCallback? onTap;
final String? tag;
final String subtitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final enabled = onTap != null;
return Material(
color: enabled ? seedGreen : seedGreen.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(10),
color: seedGreen,
borderRadius: BorderRadius.circular(18),
elevation: 3,
shadowColor: seedGreen.withValues(alpha: 0.4),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(18),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 22),
child: Row(
children: [
icon,
_IconDisc(
background: Colors.white.withValues(alpha: 0.18),
child: Icon(icon, color: Colors.white, size: 26),
),
const SizedBox(width: 16),
Expanded(
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
child: _CardText(
label: label,
subtitle: subtitle,
color: Colors.white,
subtitleColor: Colors.white.withValues(alpha: 0.85),
),
),
if (tag != null)
Text(
tag!,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
@ -116,41 +144,168 @@ class _MenuButton extends StatelessWidget {
}
}
/// A faint scatter of seed pictograms behind the menu (like mockup 02).
/// A disabled Block-2 destination: an outlined white card with a green-disc
/// icon and a "soon" tag.
class _OutlinedMenuCard extends StatelessWidget {
const _OutlinedMenuCard({
required this.icon,
required this.label,
required this.subtitle,
required this.tag,
});
final IconData icon;
final String label;
final String subtitle;
final String tag;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFB8C4AC)),
),
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 22),
child: Row(
children: [
_IconDisc(
background: const Color(0xFFDDF3CE),
child: Icon(icon, color: seedGreen, size: 26),
),
const SizedBox(width: 16),
Expanded(
child: _CardText(
label: label,
subtitle: subtitle,
color: seedOnSurface,
subtitleColor: seedMuted,
),
),
Text(
tag.toUpperCase(),
style: const TextStyle(
color: Color(0xFFB3BDA8),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
],
),
);
}
}
class _IconDisc extends StatelessWidget {
const _IconDisc({required this.background, required this.child});
final Color background;
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(shape: BoxShape.circle, color: background),
alignment: Alignment.center,
child: child,
);
}
}
class _CardText extends StatelessWidget {
const _CardText({
required this.label,
required this.subtitle,
required this.color,
required this.subtitleColor,
});
final String label;
final String subtitle;
final Color color;
final Color subtitleColor;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: color,
fontSize: 19,
fontWeight: FontWeight.w500,
),
),
Text(subtitle, style: TextStyle(color: subtitleColor, fontSize: 13)),
],
);
}
}
/// A single seed pictogram scattered onto the watermark: [ax]/[ay] are
/// fractional positions (0..1) within the layer, [angle] a slight tilt so the
/// scatter feels hand-strewn rather than gridded (like screen 00).
class _ScatteredGlyph {
const _ScatteredGlyph(this.char, this.ax, this.ay, this.size, this.angle);
final String char;
final double ax;
final double ay;
final double size;
final double angle;
}
/// A faint, disordered scatter of seed pictograms behind the menu (like screen
/// 00): glyphs strewn around the edges at staggered positions and tilts,
/// positioned by fraction so it holds up at any screen size.
class _SeedWatermark extends StatelessWidget {
const _SeedWatermark();
static const _glyphs = [
SeedGlyphs.jars,
SeedGlyphs.envelope,
SeedGlyphs.bigSpoon,
SeedGlyphs.jar,
SeedGlyphs.sack,
SeedGlyphs.scattered,
SeedGlyphs.mug,
SeedGlyphs.smallSpoon,
_ScatteredGlyph(SeedGlyphs.sack, 0.06, 0.14, 62, -0.18),
_ScatteredGlyph(SeedGlyphs.envelope, 0.82, 0.09, 48, 0.22),
_ScatteredGlyph(SeedGlyphs.scattered, 0.70, 0.30, 44, -0.12),
_ScatteredGlyph(SeedGlyphs.jar, 0.16, 0.44, 40, 0.15),
_ScatteredGlyph(SeedGlyphs.jars, 0.85, 0.66, 44, 0.18),
_ScatteredGlyph(SeedGlyphs.smallSpoon, 0.10, 0.74, 48, -0.22),
_ScatteredGlyph(SeedGlyphs.pouring, 0.40, 0.90, 52, 0.10),
_ScatteredGlyph(SeedGlyphs.mug, 0.72, 0.88, 60, -0.14),
];
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Opacity(
opacity: 0.06,
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Wrap(
spacing: 28,
runSpacing: 18,
alignment: WrapAlignment.center,
children: [
for (final g in _glyphs)
SeedGlyph(g, size: 64, color: seedGreenDark),
],
),
),
),
child: LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return Stack(
children: [
for (final g in _glyphs)
Positioned(
left: w * g.ax,
top: h * g.ay,
child: Opacity(
opacity: 0.08,
child: Transform.rotate(
angle: g.angle,
child: SeedGlyph(
g.char,
size: g.size,
color: seedGreenDark,
),
),
),
),
],
);
},
),
);
}