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,278 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
/// ───────────────────────────────────────────────────────────────────────────
/// PAGE MOTION — one vocabulary for every screen you can navigate into
///
/// Before this file the app had three different answers to "what happens when
/// I tap a row": `Get.to` with GetX's default transition, a bare
/// `MaterialPageRoute` (which on Android is the fade-through zoom, and reads as
/// a hard cut on mid-range hardware), and a `pushReplacement` with no
/// transition intent at all. Tapping *Edit Profile* looked nothing like tapping
/// *Open map*, and neither looked like anything.
///
/// Two motions, and only two, from here on:
///
/// • [MilerSlideRoute] — right → left. For the *work*: a stop's map, the
/// multi-stop route, the next step of onboarding. These are places the
/// rider goes on his way somewhere else, so they behave like pages — the
/// one below parallaxes, and a left-edge swipe goes back.
///
/// • [MilerSheetRoute] — bottom → top, and back down the same way. For
/// anything laid *over* what the rider was doing: every screen under
/// Account, plus stop verification and payment collection. It rises, it
/// covers, it drops away.
///
/// ── Why Account is all sheets ──
///
/// These screens are leaves. You open Notifications, Saved Address, FAQ or
/// Terms to do one thing, and then you are done with it — there is nowhere
/// onward to go, which is why they all close with an × rather than a back
/// arrow. A cross that dismissed a page sideways contradicted itself: the icon
/// promised a layer dropping away and the motion showed a page being left
/// behind. Now the icon and the direction say the same thing, which is what
/// Uber's account screens do.
///
/// Both routes share the same expo-out entrance — fast off the mark, long soft
/// landing, motion that arrives rather than stops. They differ on the way out,
/// and deliberately: see [kMilerExitCurve] and [kMilerSheetExitCurve].
/// ───────────────────────────────────────────────────────────────────────────
/// Fast start, long settle. The thing that makes a push feel "smooth" rather
/// than "fast" — most of the distance is covered early, so the eye reads it as
/// responsive, and the tail is slow enough that it never appears to snap.
const Curve kMilerEnterCurve = Cubic(0.16, 1.0, 0.30, 1.0);
/// Leaving a *screen* — a map, a stop, something you were working inside.
/// The rider's decision is already made, so this gets out of the way.
const Curve kMilerExitCurve = Cubic(0.4, 0.0, 0.7, 0.2);
/// Leaving a *sheet*, which is a different gesture entirely.
///
/// A task screen dropping back down is the app putting something away, and the
/// eye follows it the whole way — so it is eased at both ends rather than
/// snatched off the screen. [kMilerExitCurve] was written for a page sliding
/// sideways out of view, where nobody is watching the tail; used on a
/// full-height sheet it read as the page being deleted rather than closed.
const Curve kMilerSheetExitCurve = Cubic(0.33, 0.0, 0.15, 1.0);
// ── The 300ms ceiling ──
//
// 360/400/380 were within a hair of each other and all above the ceiling the
// motion scale sets for anything on the rider's critical path. He crosses these
// transitions 40 times a shift; 60–100ms each way is time spent watching the
// app instead of working. The curves are unchanged — they are what make the
// motion read as considered — only the duration comes down.
const Duration kMilerPushIn = DesignConstants.motionPage;
const Duration kMilerPushOut = Duration(milliseconds: 240);
const Duration kMilerSheetIn = DesignConstants.motionPage;
/// Deliberately close to the entrance, and much slower than the 290ms it was.
///
/// Dismissal had been treated as dead time to be minimised — the standard
/// instinct, and wrong here. Tapping × sent a full-height page down the screen
/// in under a third of a second, which does not read as "closing", it reads as
/// the screen vanishing. Going and coming should take about the same time,
/// because they are the same distance.
const Duration kMilerSheetOut = Duration(milliseconds: 240);
/// Right → left drill-down, with the iOS parallax on the page underneath and
/// an interactive left-edge back-swipe.
///
/// Cupertino's own 500ms is a beat too slow for a screen a rider opens forty
/// times a shift, so the duration is pulled in; the curve stays Cupertino's
/// because it is what the back-gesture physics are written against.
class MilerSlideRoute<T> extends CupertinoPageRoute<T> {
/// Off for screens whose own content wants horizontal drags — a full-bleed
/// Google Map, most of all. There, an edge swipe meant to nudge the map would
/// throw the rider out of live navigation, so the back arrow is the only way
/// out and that is the correct trade.
final bool swipeToGoBack;
MilerSlideRoute({
required super.builder,
this.swipeToGoBack = true,
super.settings,
super.maintainState,
});
@override
Duration get transitionDuration => kMilerPushIn;
@override
Duration get reverseTransitionDuration => kMilerPushOut;
@override
bool get popGestureEnabled => swipeToGoBack && super.popGestureEnabled;
}
/// Bottom → top task screen, Uber-style.
///
/// The incoming page rises the full height of the screen on the expo-out curve
/// while the page it covers eases back a few percent — the small counter-motion
/// is what stops a full-screen slab from reading as a jump-cut. When a further
/// screen is pushed on top of *this* one, it slides up and out of the way by
/// the same small amount, so a two-deep task flow still parallaxes.
class MilerSheetRoute<T> extends PageRouteBuilder<T> {
MilerSheetRoute({
required WidgetBuilder builder,
super.settings,
super.maintainState,
}) : super(
pageBuilder: (context, animation, secondaryAnimation) =>
builder(context),
transitionDuration: kMilerSheetIn,
reverseTransitionDuration: kMilerSheetOut,
// A task screen is a modal presentation: Flutter uses this to keep
// the page below from running its own outgoing slide, which would
// fight the upward motion.
fullscreenDialog: true,
transitionsBuilder: _buildSheetTransition,
);
static Widget _buildSheetTransition(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final Animation<Offset> enter =
Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero).animate(
CurvedAnimation(
parent: animation,
curve: kMilerEnterCurve,
reverseCurve: kMilerSheetExitCurve,
),
);
// Pushed over by yet another screen: retreat slightly upward instead of
// sitting frozen under the new page.
final Animation<Offset> recede =
Tween<Offset>(begin: Offset.zero, end: const Offset(0, -0.06)).animate(
CurvedAnimation(
parent: secondaryAnimation,
curve: kMilerEnterCurve,
reverseCurve: kMilerSheetExitCurve,
),
);
return SlideTransition(
position: recede,
child: SlideTransition(position: enter, child: child),
);
}
}
/// ── Call sites ──────────────────────────────────────────────────────────────
///
/// `openScreen(context, const FaqPage())` reads at the tap site as "go deeper",
/// `openSheet(context, const Profile())` as "do a thing and come back". Both
/// return the popped result, so they drop straight into the places that were
/// awaiting a `Navigator.push`.
/// Right → left. The default for anything you navigate *into*.
///
/// Pass `swipeToGoBack: false` when the destination is a map or anything else
/// that handles its own horizontal drags.
Future<T?> openScreen<T>(
BuildContext context,
Widget page, {
bool swipeToGoBack = true,
}) {
return Navigator.of(context).push<T>(
MilerSlideRoute<T>(builder: (_) => page, swipeToGoBack: swipeToGoBack),
);
}
/// Bottom → top. For task screens you complete or abandon.
Future<T?> openSheet<T>(BuildContext context, Widget page) {
return Navigator.of(
context,
).push<T>(MilerSheetRoute<T>(builder: (_) => page));
}
/// Bottom → top, replacing the current screen — for the end of a flow, where
/// going "back" to the screen you just finished would be wrong.
Future<T?> replaceWithSheet<T, TO>(BuildContext context, Widget page) {
return Navigator.of(
context,
).pushReplacement<T, TO>(MilerSheetRoute<T>(builder: (_) => page));
}
/// ───────────────────────────────────────────────────────────────────────────
/// THE SMALLER SURFACES — dialogs and bottom sheets
///
/// A page is not the only thing that arrives. The app asks for confirmation in
/// dialogs and offers choices in sheets far more often than it pushes a screen,
/// and both were using framework defaults that predate everything above.
/// ───────────────────────────────────────────────────────────────────────────
/// Bottom sheets: Flutter's default is 250ms in / 200ms out on
/// `decelerate` — quick enough to register as a pop rather than a rise, which
/// is especially wrong for the sheets a rider reads (skip reasons, stop
/// details, payment) before deciding something. Same curve as the pages, so a
/// sheet and a pushed screen feel like the same hand moved them.
///
/// Pass as `sheetAnimationStyle:` to `showModalBottomSheet`.
const AnimationStyle kMilerSheetStyle = AnimationStyle(
duration: Duration(milliseconds: 340),
curve: kMilerEnterCurve,
reverseDuration: Duration(milliseconds: 300),
reverseCurve: kMilerSheetExitCurve,
);
/// For the tall ones — a sheet that covers three-quarters of the screen is
/// closer to a page than to a picker, and travelling that far in 340ms reads as
/// a slam. Distance wants time: the same curve, given room to decelerate.
const AnimationStyle kMilerLargeSheetStyle = AnimationStyle(
duration: Duration(milliseconds: 430),
curve: kMilerEnterCurve,
reverseDuration: Duration(milliseconds: 360),
reverseCurve: kMilerSheetExitCurve,
);
/// Confirmation dialogs that grow into place instead of blinking on.
///
/// Material's `showDialog` is a 150ms straight fade with no movement at all —
/// the dialog is simply there, at full size, before the eye has found it. This
/// keeps the fade but adds the small scale-up that tells you *where* it came
/// from, and gives the barrier the same clock so the screen dims with it rather
/// than a beat ahead.
///
/// Drop-in for `showDialog`: same `context`/`builder`/`barrierDismissible`.
Future<T?> showAppDialog<T>({
required BuildContext context,
required WidgetBuilder builder,
bool barrierDismissible = true,
Color barrierColor = const Color(0x8A000000),
bool useRootNavigator = true,
}) {
return showGeneralDialog<T>(
context: context,
barrierDismissible: barrierDismissible,
barrierLabel: barrierDismissible
? MaterialLocalizations.of(context).modalBarrierDismissLabel
: null,
barrierColor: barrierColor,
useRootNavigator: useRootNavigator,
transitionDuration: const Duration(milliseconds: 260),
pageBuilder: (context, _, _) => Builder(builder: builder),
transitionBuilder: (context, animation, _, child) {
// `drive` rather than a `CurvedAnimation`: this builder runs every frame
// of the transition, and a CurvedAnimation built here would be a new
// listener-holding object each time. A CurveTween is a pure function and
// reads the same in reverse, so dismissal is the arrival played backwards.
final Animation<double> curved = animation.drive(
CurveTween(curve: Curves.easeOutCubic),
);
return FadeTransition(
opacity: curved,
child: ScaleTransition(
scale: curved.drive(Tween<double>(begin: 0.94, end: 1.0)),
child: child,
),
);
},
);
}