production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

@@ -1,55 +1,36 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.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/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.
/// FIRST RUN — three full-bleed brand slides, then sign in.
///
/// ── The copy described a different app ──
/// ── The artwork is the slide ──
///
/// 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.
/// 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.
///
/// 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.
/// ── Controls that survive three different backdrops ──
///
/// ── 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.
/// 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 _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});
@@ -60,32 +41,27 @@ class Introscreen extends StatefulWidget {
class _IntroscreenState extends State<Introscreen> {
final PageController _controller = PageController();
int _index = 0;
bool _precached = false;
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(),
),
/// Order is the order the rider sees: trust → the work → the pay.
static const _slides = <String>[
'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<void> _finish() async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -120,520 +96,194 @@ class _IntroscreenState extends State<Introscreen> {
value: SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Colors.black,
),
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),
],
),
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,
),
),
Expanded(
child: PageView.builder(
controller: _controller,
onPageChanged: (i) => setState(() => _index = i),
itemCount: _slides.length,
itemBuilder: (_, i) => _SlideView(
slide: _slides[i],
controller: _controller,
index: i,
// ── 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],
),
),
),
),
),
// ── 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),
SafeArea(
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.w700,
letterSpacing: -0.6,
height: 1.2,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
// ── 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),
],
),
),
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,
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,
),
),
],
),
),
),
);
},
);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 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);
/// 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;
TextStyle _artTitle(double size) => TextStyle(
fontSize: size.sp,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
);
const _OnboardButton({required this.label, required this.onPressed});
TextStyle _artMeta(double size) => TextStyle(
fontSize: size.sp,
fontWeight: FontWeight.w600,
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(
LucideIcons.package,
size: 11.sp,
color: ColorConstants.slateText,
),
SizedBox(width: 4.w),
Text('15', style: _artTitle(12)),
],
),
),
],
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),
),
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(LucideIcons.warehouse, size: 13.sp, color: _hubTeal),
SizedBox(width: 6.w),
Text(
'HUB',
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(
fontSize: 10.sp,
color: ColorConstants.primary,
fontSize: 16.sp,
fontWeight: FontWeight.w700,
letterSpacing: 0.6,
color: _hubTeal,
letterSpacing: -0.2,
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(LucideIcons.warehouse, 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.w700,
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,
LucideIcons.check,
),
SizedBox(height: 9.h),
_row(
2,
'Anand Traders',
'Skipped',
ColorConstants.warning,
LucideIcons.redo2,
),
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.w700,
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.w700,
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.w700,
color: accent,
fontFamily: FontConstants.fontFamily,
SizedBox(width: 8.w),
Icon(
LucideIcons.arrowRight,
size: 18.sp,
color: ColorConstants.primary,
),
),
],
],
),
),
);
}
}
/// Slide 3 — what leaves the stop with you, and where it ends up.
class _HubArt extends StatelessWidget {
/// 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 _ArtFrame(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'COLLECT AT THIS STOP',
style: TextStyle(
fontSize: 9.5.sp,
fontWeight: FontWeight.w700,
letterSpacing: 0.9,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 11.h),
_line(LucideIcons.package, '3 parcels', ColorConstants.pickupAccent),
SizedBox(height: 9.h),
_line(LucideIcons.banknote, '₹340 cash', ColorConstants.acceptGreen),
SizedBox(height: 9.h),
_line(LucideIcons.camera, '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(
LucideIcons.warehouse,
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.w700,
letterSpacing: 0.7,
color: _hubTeal,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 2.h),
Text('Hand everything over', style: _artMeta(10.5)),
],
),
),
],
),
],
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),
),
),
);
}
Widget _line(IconData icon, String text, Color accent) => Row(
children: [
Icon(icon, size: 15.sp, color: accent),
SizedBox(width: 9.w),
Text(
text,
child: Text(
'Skip',
style: TextStyle(
fontSize: 13.sp,
fontSize: 13.5.sp,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
),
),
],
);
);
}
}