import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/views/onboardscreens/Sign_in.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/Font_constant.dart'; import 'package:miler/views/helpers/constants/design_constants.dart'; /// ═════════════════════════════════════════════════════════════════════════ /// FIRST RUN — three full-bleed brand slides, then sign in. /// /// ── The artwork is the slide ── /// /// Each slide is a finished poster: headline, sub-copy, product shot and the /// MILER by DOORMILE lockup are all baked into the image. So this screen draws /// no title or body text of its own — a Flutter caption under the picture would /// simply say the same thing twice, in a second typeface, at a second size. /// The screen's whole job is to present the image edge to edge and put the /// controls somewhere they stay readable. /// /// ── Controls that survive three different backdrops ── /// /// Slides 1 and 3 end in the brand's red wave; slide 2 ends on dark tarmac. /// [ColorConstants.primary] would vanish into the first and get lost in the /// second, so the CTA inverts: a white pill carrying the red label, which holds /// its contrast on red, on grey and on black. A short scrim under the controls /// does the rest without dulling the wave. `Skip` sits top-right, where all /// three images are near-white, so it stays dark type on a light plate. /// ═════════════════════════════════════════════════════════════════════════ class Introscreen extends StatefulWidget { const Introscreen({super.key}); @override State createState() => _IntroscreenState(); } class _IntroscreenState extends State { final PageController _controller = PageController(); int _index = 0; bool _precached = false; /// Order is the order the rider sees: trust → the work → the pay. static const _slides = [ 'assets/images/onboard_1.png', 'assets/images/onboard_2.png', 'assets/images/onboard_3.png', ]; @override void didChangeDependencies() { super.didChangeDependencies(); // Decode all three up front. Paging onto an undecoded 940×1672 PNG shows a // blank frame first, which on the very first run reads as a broken app. if (_precached) return; _precached = true; for (final path in _slides) { precacheImage(AssetImage(path), context); } } Future _finish() async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('has_seen_intro', true); } catch (_) {} if (!mounted) return; Get.offAll(() => const SignIn()); } void _next() { if (_index < _slides.length - 1) { _controller.nextPage( duration: const Duration(milliseconds: 380), curve: Curves.easeOutCubic, ); } else { _finish(); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isLast = _index == _slides.length - 1; return AnnotatedRegion( value: SystemUiOverlayStyle.dark.copyWith( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, systemNavigationBarColor: Colors.black, ), child: Scaffold( backgroundColor: ColorConstants.pureSurface, // The image runs under the status bar; only the controls are inset. extendBodyBehindAppBar: true, body: Stack( fit: StackFit.expand, children: [ // ── The slide itself ── PageView.builder( controller: _controller, onPageChanged: (i) => setState(() => _index = i), itemCount: _slides.length, itemBuilder: (_, i) => Image.asset( _slides[i], fit: BoxFit.cover, alignment: Alignment.topCenter, filterQuality: FilterQuality.medium, ), ), // ── Scrim: just enough to seat the dots and the CTA ── Positioned( left: 0, right: 0, bottom: 0, child: IgnorePointer( child: Container( height: 210.h, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x00000000), Color(0x73000000)], stops: [0.0, 0.85], ), ), ), ), ), SafeArea( child: Column( children: [ // ── A way out ── Padding( padding: EdgeInsets.fromLTRB(20.w, 8.h, 16.w, 0), child: Row( children: [ const Spacer(), // Skip stays unmounted on the last slide rather than // fading to `opacity: 0` — an invisible-but-present // control is a trap for screen readers, and by then the // CTA already does what Skip would. if (!isLast) _SkipAction(onPressed: _finish), ], ), ), const Spacer(), // ── Position ── Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(_slides.length, (i) { final active = i == _index; return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, margin: EdgeInsets.symmetric(horizontal: 3.w), height: 6.h, width: active ? 26.w : 6.h, decoration: BoxDecoration( color: active ? Colors.white : Colors.white.withValues(alpha: 0.45), borderRadius: BorderRadius.circular( DesignConstants.radiusFull, ), ), ); }), ), Padding( padding: EdgeInsets.fromLTRB(24.w, 20.h, 24.w, 16.h), child: _OnboardButton( label: isLast ? 'Get started' : 'Next', onPressed: _next, ), ), ], ), ), ], ), ), ); } } /// The CTA, inverted. White plate, red label — the one pairing that holds on /// the red wave of slides 1 and 3 *and* on the tarmac of slide 2. class _OnboardButton extends StatelessWidget { final String label; final VoidCallback onPressed; const _OnboardButton({required this.label, required this.onPressed}); @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, height: ButtonSizes.primary, child: ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( backgroundColor: ColorConstants.pureSurface, foregroundColor: ColorConstants.primary, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ButtonSizes.radius.r), ), padding: EdgeInsets.symmetric(horizontal: 16.w), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: ColorConstants.primary, fontSize: 16.sp, fontWeight: FontWeight.w800, letterSpacing: -0.2, fontFamily: FontConstants.fontFamily, ), ), ), SizedBox(width: 8.w), Icon( LucideIcons.arrowRight, size: 18.sp, color: ColorConstants.primary, ), ], ), ), ); } } /// Skip, on a translucent plate. The top of every slide is near-white, so the /// label is dark type; the plate keeps it legible if artwork is ever swapped /// for something busier. class _SkipAction extends StatelessWidget { final VoidCallback onPressed; const _SkipAction({required this.onPressed}); @override Widget build(BuildContext context) { return TextButton( onPressed: onPressed, style: TextButton.styleFrom( minimumSize: Size(0, ButtonSizes.minTapTarget), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), backgroundColor: Colors.white.withValues(alpha: 0.72), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(DesignConstants.radiusFull), ), ), child: Text( 'Skip', style: TextStyle( fontSize: 13.5.sp, fontWeight: FontWeight.w800, color: ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ), ), ); } }