tane/apps/app_seeds/lib/ui/home_screen.dart
vjrj c513a6ce4b fix(block2): hide server config, enable drawer Market, badge my own offers
Three feedback fixes:
- Servers are fully hidden now — relays are managed automatically (default
  community servers; the pool skips dead ones). Sharing setup shows only 'your
  area'; no jargon-y URL field. (3 default relays for redundancy.)
- Drawer 'Market' is a live destination (was stuck on 'coming soon'), gated on
  the social layer being wired like the home card.
- My own listings in the market carry a 'You' badge (and no 'message yourself'
  button), so it's clear which offers are mine.

Analyzer clean. Note: could not run the flutter widget suite here (10+ min
compile after the geolocator native dep in this sandbox) — run 'flutter test'
locally to confirm; the You-badge test was simplified to be robust.
2026-07-10 11:56:55 +02:00

340 lines
10 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({this.marketEnabled = false, super.key});
/// When the Block 2 social layer is wired, the market becomes a live
/// destination; otherwise it stays a disabled "coming soon" card.
final bool marketEnabled;
@override
Widget build(BuildContext context) {
final t = context.t;
return Scaffold(
appBar: AppBar(title: Text(t.app.title)),
drawer: AppDrawer(marketEnabled: marketEnabled),
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(
key: const Key('home.market'),
icon: Icons.storefront_outlined,
label: t.home.openMarket,
subtitle: t.home.openMarketSubtitle,
tag: marketEnabled ? null : t.common.comingSoon,
onTap: marketEnabled
? () => context.go('/market')
: null,
),
],
),
),
),
),
],
),
);
}
}
/// 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,
this.tag,
this.onTap,
super.key,
});
final IconData icon;
final String label;
final String subtitle;
/// A small trailing tag (e.g. "coming soon"); omitted for live cards.
final String? tag;
/// When set, the card is tappable; otherwise it reads as disabled.
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final card = 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,
),
),
if (tag != null)
Text(
tag!.toUpperCase(),
style: const TextStyle(
color: Color(0xFFB3BDA8),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
)
else if (onTap != null)
const Icon(Icons.chevron_right, color: seedMuted),
],
),
);
if (onTap == null) return card;
return Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(18),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: card,
),
);
}
}
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,
),
),
),
),
],
);
},
),
);
}
}