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 '../i18n/strings.g.dart';
import 'seed_glyph.dart';
import 'theme.dart';
/// The app's navigation drawer (mockup 03). Inventory is live; the social items
/// (market, profile, chat) belong to Block 2 and are shown disabled with a
/// "coming soon" tag so the roadmap is visible.
/// The app's navigation drawer (redesign screen 05). A white sheet: Inventory is
/// the live destination (green seed glyph), the social items (market, profile,
/// chat) belong to Block 2 and are greyed with a "soon" tag, and Settings sits
/// pinned at the bottom.
class AppDrawer extends StatelessWidget {
const AppDrawer({super.key});
@ -16,34 +17,42 @@ class AppDrawer extends StatelessWidget {
Widget build(BuildContext context) {
final t = context.t;
return Drawer(
backgroundColor: Colors.white,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _DrawerHeader(),
_ActiveItem(
icon: const SeedGlyph(SeedGlyphs.jars, size: 22),
_DrawerItem(
icon: const SeedGlyph(SeedGlyphs.jars, size: 23),
label: t.menu.inventory,
onTap: () {
Navigator.of(context).pop();
context.go('/inventory');
},
),
_SoonItem(icon: Symbols.storefront, label: t.menu.market),
const Divider(),
_SoonItem(icon: Icons.person_outline, label: t.menu.profile),
_SoonItem(icon: Icons.chat_bubble_outline, label: t.menu.chat),
_SoonItem(icon: Icons.favorite_border, label: t.menu.wishlist),
_SoonItem(icon: Icons.group_outlined, label: t.menu.following),
_DrawerItem(
icon: const Icon(Symbols.storefront),
label: t.menu.market,
divider: true,
),
_DrawerItem(icon: const Icon(Icons.person), label: t.menu.profile),
_DrawerItem(
icon: const Icon(Icons.chat_bubble),
label: t.menu.chat,
),
_DrawerItem(
icon: const Icon(Icons.favorite),
label: t.menu.wishlist,
),
_DrawerItem(icon: const Icon(Icons.group), label: t.menu.following),
const Spacer(),
const Divider(),
_ActiveItem(
icon: const Icon(Icons.settings_outlined),
const Divider(height: 1),
_DrawerItem(
icon: const Icon(Icons.settings),
label: t.menu.settings,
onTap: () {
Navigator.of(context).pop();
context.go('/settings');
context.push('/settings');
},
),
],
@ -53,70 +62,110 @@ class AppDrawer extends StatelessWidget {
}
}
/// The drawer's brand header. Tapping it closes the drawer and returns to the
/// home screen.
class _DrawerHeader extends StatelessWidget {
const _DrawerHeader();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Row(
children: [
const Icon(Symbols.eco, color: seedGreen, size: 32),
const SizedBox(width: 12),
Text(
context.t.app.title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: seedGreenDark,
fontWeight: FontWeight.bold,
return InkWell(
onTap: () {
Navigator.of(context).pop();
context.go('/');
},
child: Container(
padding: const EdgeInsets.fromLTRB(22, 18, 22, 16),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: seedDivider)),
),
child: Row(
children: [
Image.asset('assets/logo.png', height: 34),
const SizedBox(width: 12),
Text(
context.t.app.title,
style: const TextStyle(
color: seedGreen,
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
),
],
],
),
),
);
}
}
class _ActiveItem extends StatelessWidget {
const _ActiveItem({
/// A 52-tall drawer row. A live destination has an [onTap] (dark text, green
/// icon); a disabled Block-2 destination is greyed with a "soon" tag. [divider]
/// draws a hairline under the row (separating live from upcoming items).
class _DrawerItem extends StatelessWidget {
const _DrawerItem({
required this.icon,
required this.label,
required this.onTap,
this.onTap,
this.divider = false,
});
final Widget icon;
final String label;
final VoidCallback onTap;
final VoidCallback? onTap;
final bool divider;
@override
Widget build(BuildContext context) {
return ListTile(
leading: SizedBox(width: 24, child: Center(child: icon)),
title: Text(label),
final enabled = onTap != null;
final fg = enabled ? seedOnSurface : const Color(0xFF9AA88F);
final iconColor = enabled ? seedGreen : const Color(0xFF9AA88F);
final row = InkWell(
onTap: onTap,
);
}
}
/// A disabled Block-2 destination with a subtle "coming soon" tag.
class _SoonItem extends StatelessWidget {
const _SoonItem({required this.icon, required this.label});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return ListTile(
enabled: false,
leading: Icon(icon),
title: Text(label),
trailing: Text(
context.t.common.comingSoon,
style: Theme.of(
context,
).textTheme.labelSmall?.copyWith(color: Colors.black38),
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 22),
decoration: divider
? const BoxDecoration(
border: Border(bottom: BorderSide(color: seedDivider)),
)
: null,
child: Row(
children: [
SizedBox(
width: 24,
child: Center(
child: IconTheme.merge(
data: IconThemeData(size: 23, color: iconColor),
child: icon,
),
),
),
const SizedBox(width: 18),
Expanded(
child: Text(
label,
style: TextStyle(
color: fg,
fontSize: 15,
fontWeight: enabled ? FontWeight.w500 : FontWeight.w400,
),
),
),
if (!enabled)
Text(
context.t.common.comingSoon.toUpperCase(),
style: const TextStyle(
color: Color(0xFFB3BDA8),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
],
),
),
);
return row;
}
}