first commit
This commit is contained in:
741
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
741
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
@@ -0,0 +1,741 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'dart:async';
|
||||
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:slide_to_submit_button/slide_to_submit_button.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:miler/views/helpers/constants/design_constants.dart';
|
||||
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
import 'package:miler/views/helpers/widgets/page_transitions.dart';
|
||||
|
||||
/// ------------------------------
|
||||
/// MAIN WIDGET WITH TWO BUTTONS
|
||||
/// ------------------------------
|
||||
class OrderStatusRow extends StatefulWidget {
|
||||
final String currentStatus;
|
||||
final Future<bool> Function(
|
||||
String newStatus, {
|
||||
String? notes,
|
||||
String? proofImagePath,
|
||||
})
|
||||
onStatusChange;
|
||||
final bool enabled;
|
||||
|
||||
/// Secondary per-booking actions rendered as compact icon buttons at the
|
||||
/// start of the action bar, so the card shows a single row of controls
|
||||
/// (Call · Details · Reject · Slide-to-Accept) instead of two stacked rows.
|
||||
/// Null hides that button. They stay tappable even while the primary
|
||||
/// slide/accept is disabled (e.g. multi-select mode) — a rider still needs to
|
||||
/// call the customer or read the parcel details.
|
||||
final VoidCallback? onCall;
|
||||
final VoidCallback? onDetails;
|
||||
|
||||
const OrderStatusRow({
|
||||
super.key,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChange,
|
||||
this.enabled = true,
|
||||
this.onCall,
|
||||
this.onDetails,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderStatusRow> createState() => _OrderStatusRowState();
|
||||
}
|
||||
|
||||
class _OrderStatusRowState extends State<OrderStatusRow> {
|
||||
bool _isProcessing = false;
|
||||
String? _overrideStatus;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant OrderStatusRow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentStatus != widget.currentStatus &&
|
||||
mounted &&
|
||||
_overrideStatus != null &&
|
||||
widget.currentStatus == _overrideStatus) {
|
||||
setState(() {
|
||||
_overrideStatus = null;
|
||||
});
|
||||
} else if (oldWidget.currentStatus != widget.currentStatus &&
|
||||
_overrideStatus != null) {
|
||||
_overrideStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
String get _effectiveStatus => _overrideStatus ?? widget.currentStatus;
|
||||
|
||||
Future<bool> _onStatusChanged(
|
||||
String newStatus, {
|
||||
String? notes,
|
||||
String? proofImagePath,
|
||||
}) async {
|
||||
debugPrint(
|
||||
'[OSR] _onStatusChanged: $newStatus, proofImagePath: $proofImagePath',
|
||||
);
|
||||
if (_isProcessing) return false;
|
||||
|
||||
// Optimistic UI: flip status immediately to avoid visible lag.
|
||||
setState(() {
|
||||
_isProcessing = true;
|
||||
_overrideStatus = newStatus;
|
||||
});
|
||||
|
||||
bool success = false;
|
||||
bool timedOut = false;
|
||||
try {
|
||||
// Hard cap wait time to keep UI from spinning indefinitely.
|
||||
success = await widget
|
||||
.onStatusChange(
|
||||
newStatus,
|
||||
notes: notes,
|
||||
proofImagePath: proofImagePath,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () {
|
||||
timedOut = true;
|
||||
// Assume success on timeout to avoid UI rollback; data will
|
||||
// refresh from server on next fetch.
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (!mounted) return success;
|
||||
setState(() {
|
||||
_isProcessing = false;
|
||||
// If API failed, revert the optimistic status.
|
||||
// If timed out, keep the optimistic status (server likely completed).
|
||||
_overrideStatus = (success || timedOut) ? newStatus : null;
|
||||
});
|
||||
return success;
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// REJECT SHEET
|
||||
// ------------------------------
|
||||
void _showRejectSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
sheetAnimationStyle: kMilerSheetStyle,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
String selectedReason = "";
|
||||
bool isLoading = false;
|
||||
final List<String> reasons = [
|
||||
"Customer not reachable",
|
||||
"Wrong address",
|
||||
"Out of service area",
|
||||
"Other reason",
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Reject booking",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Close',
|
||||
icon: Icon(
|
||||
Icons.close_rounded,
|
||||
color: ColorConstants.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (String reason in reasons)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: GestureDetector(
|
||||
onTap: () => setModalState(() {
|
||||
selectedReason = reason;
|
||||
}),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
height: ButtonSizes.primary,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason == reason
|
||||
? ColorConstants.primary
|
||||
: ColorConstants.neutralLight,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: Border.all(
|
||||
color: selectedReason == reason
|
||||
? ColorConstants.primary
|
||||
: ColorConstants.dividerColor,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
reason,
|
||||
style: TextStyle(
|
||||
color: selectedReason == reason
|
||||
? Colors.white
|
||||
: ColorConstants.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
MilerButton(
|
||||
label: "Reject booking",
|
||||
color: ColorConstants.error,
|
||||
loading: isLoading,
|
||||
onPressed: selectedReason.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
setModalState(() => isLoading = true);
|
||||
final success = await _onStatusChanged(
|
||||
"REJECTED",
|
||||
notes: selectedReason,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, success);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// CANCEL SHEET
|
||||
// ------------------------------
|
||||
void _showCancelSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
sheetAnimationStyle: kMilerSheetStyle,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
bool confirmCancel = false;
|
||||
bool isLoading = false;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Cancel this Booking?",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Once cancelled, this Booking will return to pending state.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
CheckboxListTile(
|
||||
value: confirmCancel,
|
||||
onChanged: (value) =>
|
||||
setModalState(() => confirmCancel = value ?? false),
|
||||
title: const Text("I confirm to cancel this Booking"),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
MilerButton(
|
||||
label: "Confirm cancel",
|
||||
color: ColorConstants.warning,
|
||||
loading: isLoading,
|
||||
onPressed: !confirmCancel
|
||||
? null
|
||||
: () async {
|
||||
setModalState(() => isLoading = true);
|
||||
// Capture the messenger BEFORE popping — using the
|
||||
// sheet's context after pop looks up a deactivated
|
||||
// widget and can crash.
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
// Actually cancel via the real status-change path
|
||||
// instead of just showing a fake success snackbar.
|
||||
final success = await _onStatusChanged(
|
||||
"CANCELLED",
|
||||
notes: "Cancelled by rider",
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, success);
|
||||
}
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
success
|
||||
? "Booking cancelled"
|
||||
: "Couldn't cancel. Please try again.",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String currentStatus = _effectiveStatus;
|
||||
final bool showCancel =
|
||||
currentStatus == "ARRIVED" || currentStatus == "PICKED";
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 2, 16, 12),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Single action bar: [Call] [Details] [Reject/Cancel] [Slide → …].
|
||||
// The three leading actions are compact icon buttons; the slider keeps
|
||||
// the remaining width because it's the primary action and needs room
|
||||
// to be draggable.
|
||||
Row(
|
||||
children: [
|
||||
if (widget.onCall != null) ...[
|
||||
_IconAction(
|
||||
icon: Icons.phone_rounded,
|
||||
color: ColorConstants.acceptGreen,
|
||||
tooltip: 'Call customer',
|
||||
onTap: widget.onCall,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (widget.onDetails != null) ...[
|
||||
_IconAction(
|
||||
icon: Icons.receipt_long_rounded,
|
||||
color: ColorConstants.onSurfaceVariant,
|
||||
tooltip: 'Booking details',
|
||||
onTap: widget.onDetails,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
// LEFT PRIMARY (Reject / Cancel) — compact tonal destructive
|
||||
_IconAction(
|
||||
icon: showCancel ? Icons.undo_rounded : Icons.close_rounded,
|
||||
color: showCancel
|
||||
? ColorConstants.warning
|
||||
: ColorConstants.error,
|
||||
tooltip: showCancel ? 'Cancel pickup' : 'Reject booking',
|
||||
onTap: () {
|
||||
if (showCancel) {
|
||||
_showCancelSheet(context);
|
||||
} else {
|
||||
_showRejectSheet(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// RIGHT BUTTON (ACCEPT / ARRIVED / PICKED) — takes remaining width
|
||||
OrderStatusButton(
|
||||
key: ValueKey(currentStatus),
|
||||
currentStatus: currentStatus,
|
||||
onStatusChange: (status, {proofImagePath}) =>
|
||||
_onStatusChanged(status, proofImagePath: proofImagePath),
|
||||
enabled: widget.enabled && !_isProcessing,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isProcessing)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 46,
|
||||
width: 46,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.pureSurface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
ColorConstants.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reusable rounded pill action button used by the booking status row.
|
||||
/// [filled] gives a solid brand-colored button (Accept side); otherwise it
|
||||
/// renders as a soft tonal button with an outline (Reject / Cancel side).
|
||||
class StatusPill extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool filled;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const StatusPill({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.filled = true,
|
||||
this.enabled = true,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color fg = !enabled
|
||||
? ColorConstants.disabledFill
|
||||
: (filled ? Colors.white : color);
|
||||
final Color bg = !enabled
|
||||
? ColorConstants.neutralLight
|
||||
: (filled ? color : color.withValues(alpha: 0.10));
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
child: Ink(
|
||||
height: ButtonSizes.secondary,
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: filled
|
||||
? null
|
||||
: Border.all(
|
||||
color: color.withValues(alpha: enabled ? 0.22 : 0.0),
|
||||
),
|
||||
boxShadow: (filled && enabled)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.28),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: fg, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact square icon button (50×50) used for the secondary actions in the
|
||||
/// booking action bar — Call, Details, Reject/Cancel. Tonal fill + the action's
|
||||
/// colour, so the destructive Reject reads red without a full-width label
|
||||
/// eating horizontal space the slider needs.
|
||||
class _IconAction extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
final String? tooltip;
|
||||
|
||||
const _IconAction({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
this.tooltip,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget button = Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
child: Ink(
|
||||
height: ButtonSizes.secondary,
|
||||
width: ButtonSizes.secondary,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: Border.all(color: color.withValues(alpha: 0.20)),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (tooltip == null) return button;
|
||||
return Tooltip(message: tooltip!, child: button);
|
||||
}
|
||||
}
|
||||
|
||||
/// ------------------------------
|
||||
/// STATUS BUTTON LOGIC (NO PICKUP)
|
||||
/// ------------------------------
|
||||
class OrderStatusButton extends StatefulWidget {
|
||||
final String currentStatus;
|
||||
final Future<bool> Function(String newStatus, {String? proofImagePath})
|
||||
onStatusChange;
|
||||
final bool enabled;
|
||||
|
||||
const OrderStatusButton({
|
||||
super.key,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChange,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderStatusButton> createState() => _OrderStatusButtonState();
|
||||
}
|
||||
|
||||
class _OrderStatusButtonState extends State<OrderStatusButton> {
|
||||
String get _buttonText => widget.currentStatus;
|
||||
|
||||
Color _getButtonColor() {
|
||||
switch (_buttonText) {
|
||||
case "ARRIVED":
|
||||
return ColorConstants.warning;
|
||||
case "PICKED":
|
||||
return ColorConstants.primary;
|
||||
default:
|
||||
return ColorConstants.acceptGreen;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _iconForStatus() {
|
||||
switch (_buttonText) {
|
||||
case "ARRIVED":
|
||||
return Icons.inventory_2_rounded;
|
||||
case "PICKED":
|
||||
return Icons.task_alt_rounded;
|
||||
case "ACCEPTED":
|
||||
return Icons.directions_bike_rounded;
|
||||
default:
|
||||
return Icons.check_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
// Single-step next status. Null = terminal (PICKED) or unknown — nothing to
|
||||
// advance to.
|
||||
String? get _nextStatus {
|
||||
switch (_buttonText) {
|
||||
case "ACCEPT":
|
||||
return "ACCEPTED";
|
||||
case "ACCEPTED":
|
||||
return "ARRIVED";
|
||||
case "ARRIVED":
|
||||
return "PICKED";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Friendly action verb shown on the slider, e.g. "Slide → Accept" instead of
|
||||
// the raw backend status token ("ACCEPTED").
|
||||
String get _actionLabel {
|
||||
switch (_nextStatus) {
|
||||
case "ACCEPTED":
|
||||
return "Accept";
|
||||
case "ARRIVED":
|
||||
return "Arrived";
|
||||
case "PICKED":
|
||||
return "Pick Up";
|
||||
default:
|
||||
return _nextStatus ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
bool _advancing = false;
|
||||
|
||||
/// Advances one step. PICKED requires a proof photo. Runs the same logic the
|
||||
/// old bottom-sheet slider did — just triggered inline, in one gesture.
|
||||
Future<void> _performAdvance(dynamic sliderController) async {
|
||||
final next = _nextStatus;
|
||||
if (next == null || _advancing) return;
|
||||
_advancing = true;
|
||||
try {
|
||||
if (next == "PICKED") {
|
||||
final XFile? photo = await ImagePicker().pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 50,
|
||||
);
|
||||
if (photo != null) {
|
||||
await widget.onStatusChange(next, proofImagePath: photo.path);
|
||||
}
|
||||
} else {
|
||||
await widget.onStatusChange(next);
|
||||
}
|
||||
} finally {
|
||||
_advancing = false;
|
||||
try {
|
||||
sliderController?.reset();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final next = _nextStatus;
|
||||
final color = _getButtonColor();
|
||||
|
||||
// Terminal / unknown status → a non-interactive pill (nothing to swipe).
|
||||
if (next == null) {
|
||||
return Expanded(
|
||||
child: StatusPill(
|
||||
label: _buttonText,
|
||||
icon: _iconForStatus(),
|
||||
color: color,
|
||||
filled: true,
|
||||
enabled: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Inline slide-to-advance — one gesture on the card, no bottom sheet.
|
||||
return Expanded(
|
||||
child: IgnorePointer(
|
||||
ignoring: !widget.enabled,
|
||||
child: Opacity(
|
||||
opacity: widget.enabled ? 1 : 0.5,
|
||||
child: SizedBox(
|
||||
height: ButtonSizes.secondary,
|
||||
child: SlideToSubmit.custom(
|
||||
height: ButtonSizes.secondary,
|
||||
sliderWidth: 38,
|
||||
padding: const EdgeInsets.all(5),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
),
|
||||
slider: Center(
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.pureSurface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.12),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_rounded,
|
||||
size: 18,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
hint: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Slide → $_actionLabel',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
letterSpacing: 0.2,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmit: (controller) async {
|
||||
await _performAdvance(controller);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user