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.
312 lines
9.3 KiB
Dart
312 lines
9.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../i18n/strings.g.dart';
|
|
import 'app_drawer.dart';
|
|
import 'seed_glyph.dart';
|
|
import 'theme.dart';
|
|
|
|
/// 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});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = context.t;
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(t.app.title)),
|
|
drawer: const AppDrawer(),
|
|
body: Stack(
|
|
children: [
|
|
const Positioned.fill(child: _SeedWatermark()),
|
|
Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 440),
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
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),
|
|
_OutlinedMenuCard(
|
|
icon: Icons.storefront_outlined,
|
|
label: t.home.openMarket,
|
|
subtitle: t.home.openMarketSubtitle,
|
|
tag: t.common.comingSoon,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
super.key,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String label;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
color: seedGreen,
|
|
borderRadius: BorderRadius.circular(18),
|
|
elevation: 3,
|
|
shadowColor: seedGreen.withValues(alpha: 0.4),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(18),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 22),
|
|
child: Row(
|
|
children: [
|
|
_IconDisc(
|
|
background: Colors.white.withValues(alpha: 0.18),
|
|
child: Icon(icon, color: Colors.white, size: 26),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: _CardText(
|
|
label: label,
|
|
subtitle: subtitle,
|
|
color: Colors.white,
|
|
subtitleColor: Colors.white.withValues(alpha: 0.85),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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 = [
|
|
_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: 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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|