first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

View File

@@ -0,0 +1,646 @@
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,
),
),
],
);
}