import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/views/onboardscreens/Sign_in.dart'; import 'package:miler/views/onboardscreens/auth_scaffold.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/constants/Font_constant.dart'; import 'package:miler/views/helpers/widgets/miler_app_bar.dart'; import 'package:miler/views/helpers/constants/design_constants.dart'; /// ═════════════════════════════════════════════════════════════════════════ /// FIRST RUN — three slides, then sign in. /// /// ── The copy described a different app ── /// /// This is the most important change here, and it is not a visual one. The old /// slides said "Find pickups near you — see nearby pickup requests the moment /// they arrive, accept in a single tap" and "Navigate the smart way". That is a /// gig-marketplace pitch, and Miler is not a marketplace: the **hub assigns a /// slot**, the route comes with its stops and its order already set, it is a /// **round trip** that starts and ends at the hub, and a stop that is not ready /// gets **skipped, never reordered**. A rider onboarded on the old copy would /// open the app looking for a feed of nearby jobs to claim and not find one. /// /// The third slide also carried a "Powered by AI" chip next to a claim about /// real-time bonuses. Neither was backed by anything in the app, and a promise /// made during onboarding is the one a rider remembers when it does not hold. /// /// ── The illustrations show the actual product ── /// /// Each slide used to draw a 320×320 rounded panel, containing a 156px tinted /// circle, containing an 84px Material icon — three nested shapes to present one /// glyph, on a `repeat(reverse: true)` pulse that never stopped. A location pin /// does not teach anyone what a slot route is. /// /// So the panels now hold small, honest mock-ups built from the app's own /// vocabulary — the same slot chip, stop rows and hub markers the rider will see /// on Home an hour later. Onboarding that previews the real interface is doing /// two jobs at once: it explains the model *and* it makes the real screen /// familiar on arrival. /// ═════════════════════════════════════════════════════════════════════════ class _Slide { final String title; final String body; final Widget art; const _Slide({required this.title, required this.body, required this.art}); } class Introscreen extends StatefulWidget { const Introscreen({super.key}); @override State createState() => _IntroscreenState(); } class _IntroscreenState extends State { final PageController _controller = PageController(); int _index = 0; static final _slides = <_Slide>[ _Slide( title: 'Your route arrives ready', body: 'Your hub assigns each shift as a time slot, with the stops and ' 'their order already set. Take the whole trip, or accept it stop by ' 'stop.', art: _SlotArt(), ), _Slide( title: 'Work the stops in order', body: 'Follow the list as it stands. If a stop is not ready, skip it and ' 'keep moving — the order is never rearranged underneath you.', art: _StopsArt(), ), _Slide( title: 'Collect, then close the loop', body: 'Take the parcels and any cash due, capture the proof, and finish ' 'back at the hub. Every stop is logged as you go.', art: _HubArt(), ), ]; 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, ), child: Scaffold( backgroundColor: ColorConstants.surface, body: SafeArea( child: Column( children: [ // ── Top bar: mark, and a way out ── Padding( padding: EdgeInsets.fromLTRB(20.w, 8.h, 12.w, 0), child: Row( children: [ const MilerBrandMark(), const Spacer(), // Skip stays mounted on the last slide instead of fading to // `opacity: 0` — an invisible-but-present control is a trap // for screen readers, and on the last slide the primary // button already does what Skip would. if (!isLast) AuthTextAction(label: 'Skip', onPressed: _finish), ], ), ), Expanded( child: PageView.builder( controller: _controller, onPageChanged: (i) => setState(() => _index = i), itemCount: _slides.length, itemBuilder: (_, i) => _SlideView( slide: _slides[i], controller: _controller, index: i, ), ), ), // ── 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 ? ColorConstants.primary : ColorConstants.borderStrong, borderRadius: BorderRadius.circular(DesignConstants.radiusFull), ), ); }), ), Padding( padding: EdgeInsets.fromLTRB(24.w, 22.h, 24.w, 14.h), child: AuthPrimaryButton( label: isLast ? 'Get started' : 'Next', onPressed: _next, ), ), ], ), ), ), ); } } /// One slide. Parallax is driven by an [AnimatedBuilder] on the page controller /// rather than by a listener calling `setState` on the whole screen, which is /// what the old version did on every pixel of every drag. class _SlideView extends StatelessWidget { final _Slide slide; final PageController controller; final int index; const _SlideView({ required this.slide, required this.controller, required this.index, }); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: controller, builder: (context, _) { // `page` is null until the viewport has been laid out once. final page = controller.hasClients && controller.position.haveDimensions ? (controller.page ?? controller.initialPage.toDouble()) : controller.initialPage.toDouble(); final delta = (page - index).abs().clamp(0.0, 1.0); return Opacity( opacity: 1 - (delta * 0.55), child: Transform.scale( scale: 1 - (delta * 0.08), child: SingleChildScrollView( physics: const ClampingScrollPhysics(), padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.h), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ slide.art, SizedBox(height: 34.h), Text( slide.title, textAlign: TextAlign.center, style: TextStyle( fontSize: 25.sp, fontWeight: FontWeight.w800, letterSpacing: -0.6, height: 1.2, color: ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 10.h), Text( slide.body, textAlign: TextAlign.center, style: TextStyle( fontSize: 14.5.sp, fontWeight: FontWeight.w500, height: 1.5, color: ColorConstants.secondaryText, fontFamily: FontConstants.fontFamily, ), ), ], ), ), ), ); }, ); } } // ═══════════════════════════════════════════════════════════════════════════ // ILLUSTRATIONS // // Built from the app's real vocabulary — white surfaces, 1.5px `borderStrong` // edges, slate type, the hub teal — so the first thing the rider sees is the // thing he will be using. Static: no controllers, nothing to dispose, nothing // left repainting behind the keyboard. // ═══════════════════════════════════════════════════════════════════════════ /// Shared frame: the "screen" each mock sits on. class _ArtFrame extends StatelessWidget { final Widget child; const _ArtFrame({required this.child}); @override Widget build(BuildContext context) { return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 300.w), child: Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: ColorConstants.pureSurface, borderRadius: BorderRadius.circular(DesignConstants.radiusXl), border: Border.all(color: ColorConstants.borderStrong, width: 1.5), boxShadow: const [ BoxShadow( color: Color(0x0F000000), blurRadius: 20, offset: Offset(0, 8), ), ], ), child: child, ), ), ); } } const Color _hubTeal = Color(0xFF0E7C7B); TextStyle _artTitle(double size) => TextStyle( fontSize: size.sp, fontWeight: FontWeight.w800, letterSpacing: -0.3, color: ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ); TextStyle _artMeta(double size) => TextStyle( fontSize: size.sp, fontWeight: FontWeight.w700, color: ColorConstants.secondaryText, fontFamily: FontConstants.fontFamily, ); /// Slide 1 — a slot, its load, and the shape of the trip. class _SlotArt extends StatelessWidget { @override Widget build(BuildContext context) { return _ArtFrame( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('12:00 PM – 3:00 PM', style: _artTitle(16)), SizedBox(height: 3.h), Text('6 stops', style: _artMeta(11.5)), ], ), ), Container( padding: EdgeInsets.symmetric(horizontal: 9.w, vertical: 5.h), decoration: BoxDecoration( color: ColorConstants.neutralLight, borderRadius: BorderRadius.circular(DesignConstants.radiusFull), border: Border.all(color: ColorConstants.borderStrong), ), child: Row( children: [ Icon( Icons.inventory_2_rounded, size: 11.sp, color: ColorConstants.slateText, ), SizedBox(width: 4.w), Text('15', style: _artTitle(12)), ], ), ), ], ), SizedBox(height: 14.h), // The metric strip, in miniature. Container( padding: EdgeInsets.symmetric(vertical: 8.h), decoration: BoxDecoration( borderRadius: BorderRadius.circular(DesignConstants.radiusLg), border: Border.all( color: ColorConstants.borderStrong, width: 1.5, ), ), child: Row( children: [ _cell('≈3h', 'Duration', ColorConstants.primary), _divider(), _cell('50 km', 'Round trip', ColorConstants.slateText), _divider(), _cell('₹1,030', 'To collect', ColorConstants.acceptGreen), ], ), ), SizedBox(height: 14.h), Row( children: [ Icon(Icons.warehouse_rounded, size: 13.sp, color: _hubTeal), SizedBox(width: 6.w), Text( 'HUB', style: TextStyle( fontSize: 10.sp, fontWeight: FontWeight.w800, letterSpacing: 0.6, color: _hubTeal, fontFamily: FontConstants.fontFamily, ), ), SizedBox(width: 7.w), for (var i = 0; i < 4; i++) ...[ Container( width: 14.w, height: 2, color: ColorConstants.borderStrong, ), Container( width: 7.w, height: 7.w, decoration: BoxDecoration( color: ColorConstants.primary, shape: BoxShape.circle, ), ), ], Container( width: 14.w, height: 2, color: ColorConstants.borderStrong, ), Icon(Icons.warehouse_rounded, size: 13.sp, color: _hubTeal), ], ), ], ), ); } Widget _cell(String value, String label, Color accent) => Expanded( child: Column( children: [ Text( value, style: TextStyle( fontSize: 12.5.sp, fontWeight: FontWeight.w800, letterSpacing: -0.3, color: accent, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 1.h), Text(label, style: _artMeta(9)), ], ), ); Widget _divider() => Container(width: 1, height: 24.h, color: ColorConstants.borderSubtle); } /// Slide 2 — the list, in order, with one done and one skipped. class _StopsArt extends StatelessWidget { @override Widget build(BuildContext context) { return _ArtFrame( child: Column( children: [ _row( 1, 'Sri Balaji Stores', 'Done', ColorConstants.acceptGreen, Icons.check_rounded, ), SizedBox(height: 9.h), _row( 2, 'Anand Traders', 'Skipped', ColorConstants.warning, Icons.redo_rounded, ), SizedBox(height: 9.h), _row(3, 'Kumar Textiles', 'Next', ColorConstants.primary, null), SizedBox(height: 9.h), _row(4, 'MS Enterprises', null, ColorConstants.borderStrong, null), ], ), ); } Widget _row( int n, String name, String? status, Color accent, IconData? badge, ) { final pending = status == null; return Row( children: [ Container( width: 20.w, height: 20.w, alignment: Alignment.center, decoration: BoxDecoration( color: pending ? ColorConstants.pureSurface : accent, shape: BoxShape.circle, border: Border.all(color: accent, width: 1.8), ), child: badge != null ? Icon(badge, size: 11.sp, color: Colors.white) : Text( '$n', style: TextStyle( fontSize: 10.sp, fontWeight: FontWeight.w800, color: pending ? ColorConstants.secondaryText : Colors.white, fontFamily: FontConstants.fontFamily, ), ), ), SizedBox(width: 10.w), Expanded( child: Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w800, letterSpacing: -0.2, color: pending ? ColorConstants.secondaryText : ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ), ), ), if (status != null) Text( status, style: TextStyle( fontSize: 10.5.sp, fontWeight: FontWeight.w800, color: accent, fontFamily: FontConstants.fontFamily, ), ), ], ); } } /// Slide 3 — what leaves the stop with you, and where it ends up. class _HubArt extends StatelessWidget { @override Widget build(BuildContext context) { return _ArtFrame( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'COLLECT AT THIS STOP', style: TextStyle( fontSize: 9.5.sp, fontWeight: FontWeight.w800, letterSpacing: 0.9, color: ColorConstants.secondaryText, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 11.h), _line( Icons.inventory_2_rounded, '3 parcels', ColorConstants.pickupAccent, ), SizedBox(height: 9.h), _line( Icons.payments_rounded, '₹340 cash', ColorConstants.acceptGreen, ), SizedBox(height: 9.h), _line( Icons.photo_camera_rounded, 'Photo proof', ColorConstants.slateText, ), SizedBox(height: 14.h), Container(height: 1, color: ColorConstants.borderSubtle), SizedBox(height: 13.h), Row( children: [ Container( width: 22.w, height: 22.w, alignment: Alignment.center, decoration: BoxDecoration( color: _hubTeal.withValues(alpha: 0.14), shape: BoxShape.circle, border: Border.all(color: _hubTeal, width: 1.5), ), child: Icon( Icons.warehouse_rounded, size: 12.sp, color: _hubTeal, ), ), SizedBox(width: 9.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'RETURN · HUB', style: TextStyle( fontSize: 10.sp, fontWeight: FontWeight.w800, letterSpacing: 0.7, color: _hubTeal, fontFamily: FontConstants.fontFamily, ), ), SizedBox(height: 2.h), Text('Hand everything over', style: _artMeta(10.5)), ], ), ), ], ), ], ), ); } Widget _line(IconData icon, String text, Color accent) => Row( children: [ Icon(icon, size: 15.sp, color: accent), SizedBox(width: 9.w), Text( text, style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w800, letterSpacing: -0.2, color: ColorConstants.slateText, fontFamily: FontConstants.fontFamily, ), ), ], ); }