Files
doormile_milderapp/lib/views/introscreens/introscreen.dart
2026-09-09 12:55:23 +05:30

290 lines
10 KiB
Dart
Raw 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: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<Introscreen> createState() => _IntroscreenState();
}
class _IntroscreenState extends State<Introscreen> {
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 = <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();
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,
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,
),
),
);
}
}