first commit
This commit is contained in:
202
lib/views/Dashboard/home/selection_bar.dart
Normal file
202
lib/views/Dashboard/home/selection_bar.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/design_constants.dart';
|
||||
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
import 'package:miler/widget/Bottom_page.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// SELECTION BAR — the contextual action for a multi-stop pick
|
||||
///
|
||||
/// Appears the moment the rider ticks a stop and leaves when he unticks the
|
||||
/// last one. While it is up it owns the bulk action: the inline "Accept all N
|
||||
/// stops" button in the trip's footer stands down, so there is never a choice
|
||||
/// of two bulk buttons pointing at different sets of stops.
|
||||
///
|
||||
/// ── Why this floats instead of sitting in the list ──
|
||||
///
|
||||
/// It used to be an inline button in the footer, below the stop rows. That put
|
||||
/// a *response to a direct action* at the bottom of a scroll: tick a stop near
|
||||
/// the top of a six-stop route and the button confirming what you had just done
|
||||
/// was off-screen. The rider had no feedback that the tick had registered
|
||||
/// beyond the tick itself, and no way to act on it without scrolling past every
|
||||
/// remaining stop.
|
||||
///
|
||||
/// A selection is transient and it is *modal in spirit* — the screen is in a
|
||||
/// different mode while it is non-empty. Every app that does multi-select the
|
||||
/// way riders already know it, Gmail and Google Photos included, answers that
|
||||
/// with a contextual bar pinned to the edge of the viewport: it appears with
|
||||
/// the selection, it is always reachable, and it leaves without a trace. The
|
||||
/// count lives on it, so the rider never has to go looking for how many he has
|
||||
/// picked.
|
||||
///
|
||||
/// ── Clearing ──
|
||||
///
|
||||
/// The ✕ is on the left, where a dismiss belongs and where it cannot be hit by
|
||||
/// a thumb going for Accept. Without it the only way out of a selection was to
|
||||
/// untick each stop by hand, which on a six-stop route is six taps to undo one
|
||||
/// mistake.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
class SelectionBar extends StatelessWidget {
|
||||
/// How many stops are ticked. Zero renders nothing.
|
||||
final int count;
|
||||
|
||||
/// Accepts exactly the ticked stops.
|
||||
final VoidCallback? onAccept;
|
||||
|
||||
/// Declines exactly the ticked stops, after asking why. Null hides the
|
||||
/// action.
|
||||
final VoidCallback? onReject;
|
||||
|
||||
/// Drops the whole selection.
|
||||
final VoidCallback onClear;
|
||||
|
||||
/// True while the accept is in flight.
|
||||
final bool busy;
|
||||
|
||||
const SelectionBar({
|
||||
super.key,
|
||||
required this.count,
|
||||
required this.onAccept,
|
||||
required this.onClear,
|
||||
this.onReject,
|
||||
this.busy = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Rides the floating nav bar: it is `Positioned` outside the Scaffold's
|
||||
// body, so `SafeArea` knows nothing about it and anything pinned to the
|
||||
// bottom lands underneath. See [BottomPage.bottomInset].
|
||||
//
|
||||
// A fixed inset now. This used to follow a `navVisible` notifier up and
|
||||
// down, because the bar left the screen entirely on scroll. It compacts
|
||||
// instead of leaving, so there is no longer a state in which the room is
|
||||
// free — and chasing the 20pt difference re-laid this out every time the
|
||||
// rider changed scroll direction, for a gap he would never notice.
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: BottomPage.bottomInset(context) + 10.h),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
// A real lift, not the page's usual whisper: this is the one surface
|
||||
// on Home that is temporarily above everything else, and it has to
|
||||
// read that way against a list of white cards.
|
||||
elevation: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(6.w, 8.h, 10.w, 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.pureSurface,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: Border.all(color: ColorConstants.borderSubtle),
|
||||
boxShadow: DesignConstants.shadowLg,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_ClearButton(onTap: busy ? null : onClear),
|
||||
SizedBox(width: 2.w),
|
||||
// The running count, in words — but only when there is room for
|
||||
// it. With two decisions on the bar there is not: at 320pt the
|
||||
// three fixed controls plus this line overflowed by 94pt, and
|
||||
// "Accept 3" already carries the number. So the sentence is a
|
||||
// luxury for the one-button case and the space goes to the
|
||||
// buttons otherwise.
|
||||
if (onReject == null)
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$count ${count == 1 ? 'stop' : 'stops'} selected',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.2,
|
||||
color: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
SizedBox(width: 6.w),
|
||||
// ── Both decisions, on the bar ──
|
||||
//
|
||||
// Reject used to live on every stop row and went when the rows
|
||||
// became tick-only. That left the rider able to say yes to a
|
||||
// selection and nothing else: declining a stop he could not do
|
||||
// — shop shut, address wrong — had no route at all, and the hub
|
||||
// learned about it by the stop simply never being accepted.
|
||||
//
|
||||
// So it comes back where Accept is, on the set he has picked.
|
||||
// Outlined and unlabelled-by-count against Accept's filled
|
||||
// green: they are not equal choices, and the one that ends work
|
||||
// should not look like the one that starts it.
|
||||
if (onReject != null) ...[
|
||||
Flexible(
|
||||
child: MilerButton(
|
||||
// No icon, and flexible: the width an icon costs is width
|
||||
// the two labels need side by side on a 320pt phone, and
|
||||
// under real pressure the pair should give ground evenly
|
||||
// rather than run off the edge of the bar.
|
||||
label: 'Reject',
|
||||
variant: MilerButtonVariant.outlined,
|
||||
color: ColorConstants.errorRed,
|
||||
height: ButtonSizes.secondary,
|
||||
expand: false,
|
||||
onPressed: busy ? null : onReject,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
],
|
||||
// Content-hugging, so the count on the left keeps its room on a
|
||||
// narrow phone and the label never ellipsises — a bulk action
|
||||
// that reads "Accept 3 sto…" is not one the rider will trust.
|
||||
Flexible(
|
||||
child: MilerButton(
|
||||
label: 'Accept $count',
|
||||
icon: Icons.done_all_rounded,
|
||||
color: ColorConstants.acceptGreen,
|
||||
height: ButtonSizes.secondary,
|
||||
expand: false,
|
||||
loading: busy,
|
||||
onPressed: onAccept,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClearButton extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
const _ClearButton({this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Clear selection',
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
|
||||
child: SizedBox(
|
||||
width: ButtonSizes.minTapTarget,
|
||||
height: ButtonSizes.minTapTarget,
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 20.sp,
|
||||
color: ColorConstants.secondaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user