Files
doormile_milderapp/lib/views/introscreens/introscreen.dart
2026-08-11 13:16:33 +05:30

647 lines
21 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Introscreen> createState() => _IntroscreenState();
}
class _IntroscreenState extends State<Introscreen> {
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<void> _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<SystemUiOverlayStyle>(
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,
),
),
],
);
}