723 lines
25 KiB
Dart
723 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import '../../../constant/box_name.dart';
|
|
import '../../../controller/home/map/location_search_controller.dart';
|
|
import '../../../controller/home/map/map_engine_controller.dart';
|
|
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
|
import '../../../main.dart';
|
|
import '../../widgets/elevated_btn.dart';
|
|
import '../../widgets/my_textField.dart';
|
|
import 'rp_actions.dart';
|
|
import 'rp_favorites.dart';
|
|
import 'rp_location_field.dart';
|
|
import 'rp_map_pick_overlay.dart';
|
|
import 'rp_theme.dart';
|
|
|
|
/// The route-planning surface anchored to the bottom of the map.
|
|
///
|
|
/// • While the user drags the map to place a point → [RpMapPickOverlay].
|
|
/// • Otherwise → a collapsed "Where to?" card or the full planner.
|
|
///
|
|
/// Drop-in replacement for the old `MainBottomMenuMap`. Uses only the
|
|
/// existing controller APIs; nothing in the controllers changed.
|
|
class RoutePlannerSheet extends StatelessWidget {
|
|
const RoutePlannerSheet({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GetBuilder<MapEngineController>(
|
|
builder: (mapEngine) {
|
|
if (mapEngine.isPickerShown) return const RpMapPickOverlay();
|
|
|
|
return Positioned(
|
|
left: 14,
|
|
right: 14,
|
|
bottom: Get.height * .03,
|
|
child: AnimatedContainer(
|
|
duration: RP.medium,
|
|
curve: RP.ease,
|
|
constraints: BoxConstraints(
|
|
maxHeight: mapEngine.isMainBottomMenuMap
|
|
? Get.height * 0.42
|
|
: Get.height * 0.72,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: RP.sheet,
|
|
borderRadius: BorderRadius.circular(RP.rSheet),
|
|
boxShadow: RP.sheetShadow,
|
|
border: Border.all(color: RP.divider),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: mapEngine.isMainBottomMenuMap
|
|
? const _Collapsed()
|
|
: const _Expanded(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// COLLAPSED
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
class _Collapsed extends StatelessWidget {
|
|
const _Collapsed();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final String firstName =
|
|
(box.read(BoxName.name)?.toString().split(' ').first ?? '').trim();
|
|
final mapEngine = Get.find<MapEngineController>();
|
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
|
|
|
return GetBuilder<LocationSearchController>(
|
|
builder: (locationSearch) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SizedBox(height: 12),
|
|
RP.grabber(),
|
|
const SizedBox(height: 14),
|
|
InkWell(
|
|
onTap: mapEngine.changeMainBottomMenuMap,
|
|
child: Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: RP.brand,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: RP.glow(RP.brand),
|
|
),
|
|
child: const Icon(Icons.search_rounded,
|
|
color: Colors.white, size: 24),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text.rich(
|
|
TextSpan(children: [
|
|
TextSpan(
|
|
text: '${'Where to'.tr} ',
|
|
style: TextStyle(
|
|
fontSize: 16.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: RP.textStrong),
|
|
),
|
|
if (firstName.isNotEmpty)
|
|
TextSpan(
|
|
text: firstName,
|
|
style: TextStyle(
|
|
fontSize: 16.5,
|
|
fontWeight: FontWeight.w800,
|
|
color: RP.brand),
|
|
),
|
|
const TextSpan(text: '؟'),
|
|
]),
|
|
),
|
|
if (!rideLifecycle.noCarString) ...[
|
|
const SizedBox(height: 2),
|
|
Text('Tap to search your destination'.tr,
|
|
style:
|
|
TextStyle(fontSize: 12, color: RP.textMuted)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.keyboard_arrow_up_rounded,
|
|
color: RP.textMuted, size: 26),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
RpRecentsRow(locationSearch: locationSearch),
|
|
const SizedBox(height: 16),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// EXPANDED
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
class _Expanded extends StatelessWidget {
|
|
const _Expanded();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mapEngine = Get.find<MapEngineController>();
|
|
|
|
return GetBuilder<LocationSearchController>(
|
|
builder: (s) {
|
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
|
final bool isOther = rideLifecycle.isAnotherOreder;
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const SizedBox(height: 12),
|
|
Center(child: RP.grabber()),
|
|
// Header
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 14, 14, 6),
|
|
child: Row(
|
|
children: [
|
|
Text('Plan Your Route'.tr, style: RP.title),
|
|
const Spacer(),
|
|
_CircleIconButton(
|
|
icon: Icons.keyboard_arrow_down_rounded,
|
|
onTap: mapEngine.changeMainBottomMenuMap,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Order-type toggle
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 6, 16, 4),
|
|
child: _OrderTypeToggle(isOther: isOther),
|
|
),
|
|
|
|
// Route timeline
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 10, 16, 4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_originField(s, isOther),
|
|
const SizedBox(height: 10),
|
|
...List.generate(
|
|
s.activeMenuWaypointCount,
|
|
(i) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: _StopRow(index: i, s: s),
|
|
),
|
|
),
|
|
if (s.activeMenuWaypointCount < 2) ...[
|
|
_AddStopButton(onTap: RpActions.addStop),
|
|
const SizedBox(height: 10),
|
|
],
|
|
_destinationField(s),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 6),
|
|
_Divider(),
|
|
|
|
// Quick access
|
|
_SectionHeader(
|
|
title: 'Quick Access'.tr,
|
|
trailing: const RpFavoritesButton(),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(child: _homeWorkButton(isWork: false)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _homeWorkButton(isWork: true)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
RpRecentsRow(locationSearch: s),
|
|
|
|
const SizedBox(height: 6),
|
|
_Divider(),
|
|
|
|
// Advanced
|
|
_SectionHeader(title: 'Advanced Tools'.tr),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: _WhatsAppLinkButton(locationSearch: s),
|
|
),
|
|
const SizedBox(height: 22),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ── Origin ──────────────────────────────────────────────────────────────
|
|
Widget _originField(LocationSearchController s, bool isOther) {
|
|
if (isOther) {
|
|
return RpLocationField(
|
|
accent: RP.origin,
|
|
icon: Icons.person_pin_circle_rounded,
|
|
controller: s.placeStartController,
|
|
hintText: 'Search for a starting point'.tr,
|
|
results: s.placesStart,
|
|
onQuery: (_) => s.getPlacesStart(),
|
|
onEmpty: s.clearPlacesStart,
|
|
onPickOnMap: RpActions.pickOtherPickupOnMap,
|
|
onResultTap: (i) => RpActions.selectStart(s.placesStart[i]),
|
|
);
|
|
}
|
|
// Regular order: origin = current location, changeable on the map.
|
|
return RpLocationField(
|
|
accent: RP.origin,
|
|
icon: Icons.my_location_rounded,
|
|
controller: s.placeStartController,
|
|
hintText: s.currentLocationString,
|
|
results: const [],
|
|
searchable: false,
|
|
onQuery: (_) {},
|
|
onEmpty: () {},
|
|
onPickOnMap: RpActions.pickMyStartOnMap,
|
|
onResultTap: (_) {},
|
|
);
|
|
}
|
|
|
|
// ── Destination ─────────────────────────────────────────────────────────
|
|
Widget _destinationField(LocationSearchController s) {
|
|
final mapEngine = Get.find<MapEngineController>();
|
|
return RpLocationField(
|
|
accent: RP.destination,
|
|
icon: Icons.flag_rounded,
|
|
controller: s.placeDestinationController,
|
|
hintText: s.hintTextDestinationPoint,
|
|
results: s.placesDestination,
|
|
onQuery: (_) {
|
|
s.getPlaces();
|
|
mapEngine.changeHeightPlaces();
|
|
},
|
|
onEmpty: () {
|
|
s.clearPlacesDestination();
|
|
mapEngine.changeHeightPlaces();
|
|
},
|
|
onPickOnMap: RpActions.pickDestinationOnMap,
|
|
onResultTap: (i) => RpActions.selectDestination(i, s.placesDestination[i]),
|
|
onResultFavorite: (i) {
|
|
final res = s.placesDestination[i];
|
|
RpActions.addFavorite(
|
|
Get.context!,
|
|
res['latitude'],
|
|
res['longitude'],
|
|
(res['name_ar'] ?? res['name'] ?? 'Unknown Place').toString(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _homeWorkButton({required bool isWork}) {
|
|
final String saved = isWork
|
|
? (box.read(BoxName.addWork)?.toString() ?? 'addWork')
|
|
: (box.read(BoxName.addHome)?.toString() ?? 'addHome');
|
|
final bool isSet = isWork ? saved != 'addWork' : saved != 'addHome';
|
|
final Color accent =
|
|
isWork ? const Color(0xFF2563EB) : const Color(0xFFF59E0B);
|
|
|
|
return _QuickTile(
|
|
icon: isWork ? Icons.work_rounded : Icons.home_rounded,
|
|
accent: accent,
|
|
label: isWork
|
|
? (isSet ? 'To Work'.tr : 'Add Work'.tr)
|
|
: (isSet ? 'To Home'.tr : 'Add Home'.tr),
|
|
onTap: () {
|
|
if (isSet) {
|
|
RpActions.goToSaved(
|
|
isWork ? BoxName.addWork : BoxName.addHome,
|
|
isWork ? 'To Work' : 'To Home');
|
|
} else {
|
|
RpActions.pickHomeOrWork(isWork: isWork);
|
|
}
|
|
},
|
|
onLongPress: () => RpActions.changeHomeOrWork(isWork: isWork),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Stop row ────────────────────────────────────────────────────────────────
|
|
class _StopRow extends StatelessWidget {
|
|
final int index;
|
|
final LocationSearchController s;
|
|
const _StopRow({required this.index, required this.s});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Color accent = RP.stopColor(index);
|
|
final bool isSet = s.menuWaypoints[index] != null;
|
|
final String name =
|
|
isSet ? s.menuWaypointNames[index] : '${'Stop'.tr} ${index + 1}';
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: RP.fieldFill,
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
border: Border.all(
|
|
color: isSet ? accent.withValues(alpha: 0.45) : Colors.transparent,
|
|
width: 1.4,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 34,
|
|
height: 34,
|
|
decoration: BoxDecoration(
|
|
color: accent,
|
|
shape: BoxShape.circle,
|
|
boxShadow: RP.glow(accent),
|
|
),
|
|
child: Center(
|
|
child: Text('${index + 1}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w800)),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => RpActions.pickStopOnMap(index),
|
|
child: Text(
|
|
name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: isSet
|
|
? RP.fieldValue
|
|
: RP.fieldHint,
|
|
),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () => RpActions.removeStop(index),
|
|
borderRadius: BorderRadius.circular(RP.rChip),
|
|
child: Container(
|
|
width: 34,
|
|
height: 34,
|
|
alignment: Alignment.center,
|
|
child: Icon(Icons.delete_outline_rounded,
|
|
color: RP.destination, size: 19),
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () => RpActions.pickStopOnMap(index),
|
|
borderRadius: BorderRadius.circular(RP.rChip),
|
|
child: Container(
|
|
width: 34,
|
|
height: 34,
|
|
decoration: BoxDecoration(
|
|
color: accent.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(RP.rChip),
|
|
),
|
|
child: Icon(Icons.map_rounded, color: accent, size: 19),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddStopButton extends StatelessWidget {
|
|
final VoidCallback onTap;
|
|
const _AddStopButton({required this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 13),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
border: Border.all(
|
|
color: RP.stop1.withValues(alpha: 0.5),
|
|
width: 1.4,
|
|
style: BorderStyle.solid),
|
|
color: RP.stop1.withValues(alpha: 0.06),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.add_location_alt_outlined,
|
|
color: RP.stop1, size: 18),
|
|
const SizedBox(width: 8),
|
|
Text('Add a Stop'.tr,
|
|
style: TextStyle(
|
|
color: RP.stop1,
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w700)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Order-type toggle ────────────────────────────────────────────────────────
|
|
class _OrderTypeToggle extends StatelessWidget {
|
|
final bool isOther;
|
|
const _OrderTypeToggle({required this.isOther});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mapEngine = Get.find<MapEngineController>();
|
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
|
|
|
void select(bool other) {
|
|
if (mapEngine.isAnotherOreder == other) return;
|
|
mapEngine.changeisAnotherOreder(other);
|
|
rideLifecycle.isAnotherOreder = other;
|
|
// The expanded planner rebuilds on LocationSearchController updates, so
|
|
// refresh it here to swap the origin field (self ⇄ other) instantly.
|
|
Get.find<LocationSearchController>().update();
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: RP.chipFill,
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_seg(
|
|
selected: !isOther,
|
|
icon: Icons.person_rounded,
|
|
label: 'Order for myself'.tr,
|
|
onTap: () => select(false),
|
|
),
|
|
const SizedBox(width: 4),
|
|
_seg(
|
|
selected: isOther,
|
|
icon: Icons.group_rounded,
|
|
label: 'Order for someone else'.tr,
|
|
onTap: () => select(true),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _seg({
|
|
required bool selected,
|
|
required IconData icon,
|
|
required String label,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return Expanded(
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(RP.rChip),
|
|
child: AnimatedContainer(
|
|
duration: RP.fast,
|
|
padding: const EdgeInsets.symmetric(vertical: 11),
|
|
decoration: BoxDecoration(
|
|
color: selected ? const Color(0xFF4F46E5) : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(RP.rChip),
|
|
boxShadow: selected
|
|
? RP.glow(const Color(0xFF4F46E5), intensity: 0.35)
|
|
: null,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon,
|
|
size: 16,
|
|
color: selected ? Colors.white : RP.textMuted),
|
|
const SizedBox(width: 7),
|
|
Flexible(
|
|
child: Text(
|
|
label,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 12.5,
|
|
fontWeight:
|
|
selected ? FontWeight.w700 : FontWeight.w600,
|
|
color: selected ? Colors.white : RP.textBody,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Quick tile (home/work) ───────────────────────────────────────────────────
|
|
class _QuickTile extends StatelessWidget {
|
|
final IconData icon;
|
|
final Color accent;
|
|
final String label;
|
|
final VoidCallback onTap;
|
|
final VoidCallback? onLongPress;
|
|
const _QuickTile({
|
|
required this.icon,
|
|
required this.accent,
|
|
required this.label,
|
|
required this.onTap,
|
|
this.onLongPress,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
onLongPress: onLongPress,
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
|
decoration: BoxDecoration(
|
|
color: accent.withValues(alpha: 0.10),
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: accent, size: 20),
|
|
const SizedBox(width: 10),
|
|
Flexible(
|
|
child: Text(label,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: accent,
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w700)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── WhatsApp link ─────────────────────────────────────────────────────────────
|
|
class _WhatsAppLinkButton extends StatelessWidget {
|
|
final LocationSearchController locationSearch;
|
|
const _WhatsAppLinkButton({required this.locationSearch});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
onTap: () {
|
|
Get.dialog(
|
|
AlertDialog(
|
|
backgroundColor: RP.sheetElevated,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(RP.rCard)),
|
|
title: Text('WhatsApp Location Extractor'.tr, style: RP.title),
|
|
content: Form(
|
|
key: locationSearch.sosFormKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
MyTextForm(
|
|
controller: locationSearch.whatsAppLocationText,
|
|
label: 'Location Link'.tr,
|
|
type: TextInputType.url,
|
|
hint: 'https://maps.app.goo.gl/...'),
|
|
const SizedBox(height: 16),
|
|
MyElevatedButton(
|
|
title: 'Go to this location'.tr,
|
|
onPressed: () => locationSearch.goToWhatappLocation()),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF25D366).withValues(alpha: 0.10),
|
|
borderRadius: BorderRadius.circular(RP.rField),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.link_rounded,
|
|
color: Color(0xFF1FA855), size: 20),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text('Paste WhatsApp location link'.tr,
|
|
style: const TextStyle(
|
|
color: Color(0xFF1FA855),
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w700)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Small shared bits ─────────────────────────────────────────────────────────
|
|
class _SectionHeader extends StatelessWidget {
|
|
final String title;
|
|
final Widget? trailing;
|
|
const _SectionHeader({required this.title, this.trailing});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 4, 12, 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(title.toUpperCase(), style: RP.sectionLabel),
|
|
if (trailing != null) trailing!,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Divider extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: 1,
|
|
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
color: RP.divider,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CircleIconButton extends StatelessWidget {
|
|
final IconData icon;
|
|
final VoidCallback onTap;
|
|
const _CircleIconButton({required this.icon, required this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(RP.rPill),
|
|
child: Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(color: RP.fieldFill, shape: BoxShape.circle),
|
|
child: Icon(icon, size: 24, color: RP.textBody),
|
|
),
|
|
);
|
|
}
|
|
}
|