import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../i18n/strings.g.dart'; import 'seed_glyph.dart'; import 'theme.dart'; /// One onboarding page: a large emblem (a bundled seed glyph or a Material /// symbol), a title and a short body. Illustrations use the app's own icon set /// rather than photos — no image licensing, and it keeps the bundle light. class _Slide { const _Slide({ this.glyph, this.icon, required this.title, required this.body, }); /// A `seedks` glyph char (see [SeedGlyphs]); mutually exclusive with [icon]. final String? glyph; /// A Material symbol, used where no seed glyph fits (privacy, sprout). final IconData? icon; final String title; final String body; } /// The first-run intro carousel (also reachable later from the drawer): a few /// swipeable cards explaining what Tanemaki is, how privacy works, how sharing /// works and the Plantare. [onDone] fires when the user skips or finishes, and /// is where the caller marks the intro as seen and leaves the screen. class IntroScreen extends StatefulWidget { const IntroScreen({required this.onDone, super.key}); final VoidCallback onDone; @override State createState() => _IntroScreenState(); } class _IntroScreenState extends State { final _controller = PageController(); int _page = 0; @override void dispose() { _controller.dispose(); super.dispose(); } List<_Slide> _slides(Translations t) => [ _Slide( glyph: SeedGlyphs.scattered, title: t.intro.slides.welcome.title, body: t.intro.slides.welcome.body, ), _Slide( glyph: SeedGlyphs.jars, title: t.intro.slides.inventory.title, body: t.intro.slides.inventory.body, ), _Slide( icon: Icons.lock_outline, title: t.intro.slides.privacy.title, body: t.intro.slides.privacy.body, ), _Slide( glyph: SeedGlyphs.share, title: t.intro.slides.share.title, body: t.intro.slides.share.body, ), _Slide( icon: Symbols.potted_plant, title: t.intro.slides.plantare.title, body: t.intro.slides.plantare.body, ), ]; void _next(int count) { if (_page >= count - 1) { widget.onDone(); return; } _controller.nextPage( duration: const Duration(milliseconds: 280), curve: Curves.easeInOut, ); } @override Widget build(BuildContext context) { final t = context.t; final slides = _slides(t); final isLast = _page == slides.length - 1; return Scaffold( backgroundColor: seedCanvas, body: SafeArea( child: Column( children: [ Align( alignment: AlignmentDirectional.centerEnd, child: TextButton( onPressed: widget.onDone, child: Text(t.intro.skip), ), ), Expanded( child: PageView.builder( controller: _controller, onPageChanged: (i) => setState(() => _page = i), itemCount: slides.length, itemBuilder: (context, i) => _SlideView(slide: slides[i]), ), ), _Dots(count: slides.length, active: _page), Padding( padding: const EdgeInsets.fromLTRB(24, 20, 24, 24), child: SizedBox( width: double.infinity, child: FilledButton( onPressed: () => _next(slides.length), child: Text(isLast ? t.intro.start : t.intro.next), ), ), ), ], ), ), ); } } /// The centred emblem + title + body for a single [slide]. class _SlideView extends StatelessWidget { const _SlideView({required this.slide}); final _Slide slide; @override Widget build(BuildContext context) { // Centre when there's room, scroll when the viewport is short (small phones // in landscape, split-screen) so the card never overflows. return LayoutBuilder( builder: (context, constraints) => SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 160, height: 160, decoration: const BoxDecoration( color: seedPrimaryContainer, shape: BoxShape.circle, ), alignment: Alignment.center, child: slide.glyph != null ? SeedGlyph(slide.glyph!, size: 76, color: seedGreen) : Icon(slide.icon, size: 72, color: seedGreen), ), const SizedBox(height: 36), Text( slide.title, textAlign: TextAlign.center, style: const TextStyle( color: seedTitle, fontSize: 24, fontWeight: FontWeight.w600, height: 1.2, ), ), const SizedBox(height: 14), Text( slide.body, textAlign: TextAlign.center, style: const TextStyle( color: seedOnSurfaceVariant, fontSize: 16, height: 1.45, ), ), ], ), ), ), ), ), ), ); } } /// The page-position dots: the active one is a wider green pill. class _Dots extends StatelessWidget { const _Dots({required this.count, required this.active}); final int count; final int active; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (var i = 0; i < count; i++) AnimatedContainer( duration: const Duration(milliseconds: 220), margin: const EdgeInsets.symmetric(horizontal: 4), width: i == active ? 22 : 8, height: 8, decoration: BoxDecoration( color: i == active ? seedGreen : seedOutline, borderRadius: BorderRadius.circular(4), ), ), ], ); } }