@ -1,58 +1,72 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file("local.properties")
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader("UTF-8") { reader ->
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = "1"
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty("flutter.versionName")
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = "1.0"
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
namespace = "com.arminta.supplier_new"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
compileSdkVersion 34
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
freeCompilerArgs += ['-Xskip-metadata-version-check']
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.arminta.supplier_new"
|
||||
applicationId "com.arminta.supplier_new"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutterVersionCode.toInteger()
|
||||
versionName = flutterVersionName
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
minSdkVersion 28
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.debug
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 375 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 759 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 522 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 759 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.6 KiB |
@ -0,0 +1,98 @@
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
|
||||
class LocationPage extends StatefulWidget {
|
||||
const LocationPage({Key? key}) : super(key: key);
|
||||
@override
|
||||
State<LocationPage> createState() => _LocationPageState();
|
||||
}
|
||||
|
||||
class _LocationPageState extends State<LocationPage> {
|
||||
String? _currentAddress;
|
||||
Position? _currentPosition;
|
||||
|
||||
Future<bool> _handleLocationPermission() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'Location services are disabled. Please enable the services')));
|
||||
return false;
|
||||
}
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Location permissions are denied')));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'Location permissions are permanently denied, we cannot request permissions.')));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _getCurrentPosition() async {
|
||||
final hasPermission = await _handleLocationPermission();
|
||||
|
||||
if (!hasPermission) return;
|
||||
await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high)
|
||||
.then((Position position) {
|
||||
setState(() => _currentPosition = position);
|
||||
_getAddressFromLatLng(_currentPosition!);
|
||||
}).catchError((e) {
|
||||
debugPrint(e);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _getAddressFromLatLng(Position position) async {
|
||||
await placemarkFromCoordinates(
|
||||
_currentPosition!.latitude, _currentPosition!.longitude)
|
||||
.then((List<Placemark> placemarks) {
|
||||
|
||||
Placemark place = placemarks[0];
|
||||
_currentAddress = '${place.street}, ${place.subLocality}, ${place.locality}, ${place.postalCode}, ${place.country}';
|
||||
print(place);
|
||||
|
||||
setState(() {
|
||||
});
|
||||
}).catchError((e) {
|
||||
debugPrint(e);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Location Page")),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('LAT: ${_currentPosition?.latitude ?? ""}'),
|
||||
Text('LNG: ${_currentPosition?.longitude ?? ""}'),
|
||||
Text('ADDRESS: ${_currentAddress ?? ""}'),
|
||||
const SizedBox(height:32),
|
||||
ElevatedButton(
|
||||
onPressed: _getCurrentPosition,
|
||||
child: const Text("Get Current Location"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
class APIKeys {
|
||||
APIKeys._();
|
||||
|
||||
static String androidApiKey = "AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM";
|
||||
static String iosApiKey = "YOUR IOS KEY HERE";
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supplier_new/common/settings.dart';
|
||||
|
||||
|
||||
class FinancialMainScreen extends StatefulWidget {
|
||||
const FinancialMainScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FinancialMainScreen> createState() => _FinancialMainScreenState();
|
||||
}
|
||||
|
||||
class _FinancialMainScreenState extends State<FinancialMainScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
final transactions = [
|
||||
{
|
||||
"name": "My Home Bhooja",
|
||||
"date": "21 August",
|
||||
"amount": "+ ₹2,580",
|
||||
"color": Colors.green,
|
||||
"status": "success",
|
||||
},
|
||||
{
|
||||
"name": "Ramakrishna",
|
||||
"date": "20 August",
|
||||
"amount": "- ₹748",
|
||||
"color": Colors.red,
|
||||
"status": "success",
|
||||
},
|
||||
{
|
||||
"name": "Malleshan Water Supplies",
|
||||
"date": "21 August",
|
||||
"amount": "- ₹10,000",
|
||||
"color": Colors.red,
|
||||
"status": "success",
|
||||
},
|
||||
{
|
||||
"name": "My Home Bhooja",
|
||||
"date": "21 August",
|
||||
"amount": "+ ₹2,580",
|
||||
"color": Colors.grey,
|
||||
"status": "failed",
|
||||
},
|
||||
{
|
||||
"name": "My Home Bhooja",
|
||||
"date": "21 August",
|
||||
"amount": "+ ₹2,580",
|
||||
"color": Colors.green,
|
||||
"status": "success",
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
Widget Transactions(){
|
||||
return Container(
|
||||
color: Color(0XFFFFFFFF),
|
||||
child:Column(
|
||||
children: [
|
||||
// Filters row
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterChip("Status"),
|
||||
_buildFilterChip("Payment Method"),
|
||||
_buildFilterChip("Date"),
|
||||
_buildFilterChip("Amount"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// Transactions list
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: transactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final txn = transactions[index];
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.blue,
|
||||
child: Icon(Icons.person, color: Colors.white),
|
||||
),
|
||||
title: Text(txn["name"].toString()),
|
||||
subtitle: Text(txn["date"].toString()),
|
||||
trailing: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
txn["amount"].toString(),
|
||||
style: TextStyle(
|
||||
color: txn["color"] as Color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (txn["status"] == "failed")
|
||||
const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.red, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
"Failed",
|
||||
style: TextStyle(
|
||||
color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget CreditAccounts(){
|
||||
return Container(
|
||||
color: Color(0XFFFFFFFF),
|
||||
child:Center(
|
||||
child: Text("Credit Accounts Coming Soon..."),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar:AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
'Financials',
|
||||
style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w500),
|
||||
),
|
||||
iconTheme: IconThemeData(color: Color(0XFF2A2A2A)),
|
||||
leading: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
8, 8, 8, 8), // Add padding if needed
|
||||
child: Image.asset(
|
||||
'images/backbutton_appbar.png',
|
||||
height: 24,
|
||||
width: 24,// Replace with your image path
|
||||
fit: BoxFit.contain, // Adjust the fit
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(50.0),
|
||||
child: Container(
|
||||
height: 38,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: AnimatedBuilder(
|
||||
animation: _tabController,
|
||||
builder: (context, _) {
|
||||
return TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: Colors.transparent, // remove underline
|
||||
dividerColor: Colors.transparent,
|
||||
isScrollable: false,
|
||||
overlayColor: MaterialStateProperty.all(Colors.transparent),// equal width
|
||||
tabs: List.generate(2, (index) {
|
||||
final labels = ['Transactions', 'Credit Accounts'];
|
||||
final isSelected = _tabController.index == index;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0XFFF1F1F1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(27),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
labels[index],
|
||||
style: fontTextStyle(
|
||||
12,
|
||||
const Color(0XFF101214),
|
||||
FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// Tab 1: Transactions
|
||||
Transactions(),
|
||||
|
||||
// Tab 2: Credit Accounts
|
||||
CreditAccounts()
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FilterChip(
|
||||
label: Text(text),
|
||||
onSelected: (val) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
library google_maps_place_picker_mb;
|
||||
|
||||
export 'src/models/pick_result.dart';
|
||||
export 'src/components/floating_card.dart';
|
||||
export 'src/components/rounded_frame.dart';
|
||||
export 'src/models/circle_area.dart';
|
||||
export 'src/place_picker.dart';
|
||||
@ -0,0 +1,161 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_maps_webservice/geocoding.dart';
|
||||
import 'package:google_maps_webservice/places.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../src/models/pick_result.dart';
|
||||
import '../src/place_picker.dart';
|
||||
|
||||
class PlaceProvider extends ChangeNotifier {
|
||||
PlaceProvider(
|
||||
String apiKey,
|
||||
String? proxyBaseUrl,
|
||||
Client? httpClient,
|
||||
Map<String, dynamic> apiHeaders,
|
||||
) {
|
||||
places = GoogleMapsPlaces(
|
||||
apiKey: apiKey,
|
||||
baseUrl: proxyBaseUrl,
|
||||
httpClient: httpClient,
|
||||
apiHeaders: apiHeaders as Map<String, String>?,
|
||||
);
|
||||
|
||||
geocoding = GoogleMapsGeocoding(
|
||||
apiKey: apiKey,
|
||||
baseUrl: proxyBaseUrl,
|
||||
httpClient: httpClient,
|
||||
apiHeaders: apiHeaders as Map<String, String>?,
|
||||
);
|
||||
}
|
||||
|
||||
static PlaceProvider of(BuildContext context, {bool listen = true}) =>
|
||||
Provider.of<PlaceProvider>(context, listen: listen);
|
||||
|
||||
late GoogleMapsPlaces places;
|
||||
late GoogleMapsGeocoding geocoding;
|
||||
String? sessionToken;
|
||||
bool isOnUpdateLocationCooldown = false;
|
||||
LocationAccuracy? desiredAccuracy;
|
||||
bool isAutoCompleteSearching = false;
|
||||
|
||||
Future<void> updateCurrentLocation(bool forceAndroidLocationManager) async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
// Test if location services are enabled.
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
// Location services are not enabled don't continue
|
||||
// accessing the position and request users of the
|
||||
// App to enable the location services.
|
||||
return Future.error('Location services are disabled.');
|
||||
}
|
||||
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
// Permissions are denied, next time you could try
|
||||
// requesting permissions again (this is also where
|
||||
// Android's shouldShowRequestPermissionRationale
|
||||
// returned true. According to Android guidelines
|
||||
// your App should show an explanatory UI now.
|
||||
return Future.error('Location permissions are denied');
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
// Permissions are denied forever, handle appropriately.
|
||||
return Future.error(
|
||||
'Location permissions are permanently denied, we cannot request permissions.');
|
||||
}
|
||||
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.best,
|
||||
);
|
||||
|
||||
currentPosition = position;
|
||||
|
||||
// notifyListeners();
|
||||
}
|
||||
|
||||
Position? _currentPosition;
|
||||
Position? get currentPosition => _currentPosition;
|
||||
set currentPosition(Position? newPosition) {
|
||||
_currentPosition = newPosition;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Timer? _debounceTimer;
|
||||
Timer? get debounceTimer => _debounceTimer;
|
||||
set debounceTimer(Timer? timer) {
|
||||
_debounceTimer = timer;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
CameraPosition? _previousCameraPosition;
|
||||
CameraPosition? get prevCameraPosition => _previousCameraPosition;
|
||||
setPrevCameraPosition(CameraPosition? prePosition) {
|
||||
_previousCameraPosition = prePosition;
|
||||
}
|
||||
|
||||
CameraPosition? _currentCameraPosition;
|
||||
CameraPosition? get cameraPosition => _currentCameraPosition;
|
||||
setCameraPosition(CameraPosition? newPosition) {
|
||||
_currentCameraPosition = newPosition;
|
||||
}
|
||||
|
||||
PickResult? _selectedPlace;
|
||||
PickResult? get selectedPlace => _selectedPlace;
|
||||
set selectedPlace(PickResult? result) {
|
||||
_selectedPlace = result;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SearchingState _placeSearchingState = SearchingState.Idle;
|
||||
SearchingState get placeSearchingState => _placeSearchingState;
|
||||
set placeSearchingState(SearchingState newState) {
|
||||
_placeSearchingState = newState;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
GoogleMapController? _mapController;
|
||||
GoogleMapController? get mapController => _mapController;
|
||||
set mapController(GoogleMapController? controller) {
|
||||
_mapController = controller;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
PinState _pinState = PinState.Preparing;
|
||||
PinState get pinState => _pinState;
|
||||
set pinState(PinState newState) {
|
||||
_pinState = newState;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _isSeachBarFocused = false;
|
||||
bool get isSearchBarFocused => _isSeachBarFocused;
|
||||
set isSearchBarFocused(bool focused) {
|
||||
_isSeachBarFocused = focused;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
MapType _mapType = MapType.normal;
|
||||
MapType get mapType => _mapType;
|
||||
setMapType(MapType mapType, {bool notify = false}) {
|
||||
_mapType = mapType;
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
switchMapType() {
|
||||
_mapType = MapType.values[(_mapType.index + 1) % MapType.values.length];
|
||||
if (_mapType == MapType.none) _mapType = MapType.normal;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SearchProvider extends ChangeNotifier {
|
||||
static SearchProvider of(BuildContext context, {bool listen = true}) =>
|
||||
Provider.of<SearchProvider>(context, listen: listen);
|
||||
|
||||
String prevSearchTerm = "";
|
||||
String _searchTerm = "";
|
||||
String get searchTerm => _searchTerm;
|
||||
|
||||
set searchTerm(String newValue) {
|
||||
_searchTerm = newValue;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,341 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_webservice/places.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/google_maps_place_picker.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/providers/place_provider.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/providers/search_provider.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/components/prediction_tile.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/components/rounded_frame.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/controllers/autocomplete_search_controller.dart';
|
||||
|
||||
class AutoCompleteSearch extends StatefulWidget {
|
||||
const AutoCompleteSearch(
|
||||
{Key? key,
|
||||
required this.sessionToken,
|
||||
required this.onPicked,
|
||||
required this.appBarKey,
|
||||
this.hintText = "Search here",
|
||||
this.searchingText = "Searching...",
|
||||
this.hidden = false,
|
||||
this.height = 40,
|
||||
this.contentPadding = EdgeInsets.zero,
|
||||
this.debounceMilliseconds,
|
||||
this.onSearchFailed,
|
||||
required this.searchBarController,
|
||||
this.autocompleteOffset,
|
||||
this.autocompleteRadius,
|
||||
this.autocompleteLanguage,
|
||||
this.autocompleteComponents,
|
||||
this.autocompleteTypes,
|
||||
this.strictbounds,
|
||||
this.region,
|
||||
this.initialSearchString,
|
||||
this.searchForInitialValue,
|
||||
this.autocompleteOnTrailingWhitespace})
|
||||
: super(key: key);
|
||||
|
||||
final String? sessionToken;
|
||||
final String? hintText;
|
||||
final String? searchingText;
|
||||
final bool hidden;
|
||||
final double height;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final int? debounceMilliseconds;
|
||||
final ValueChanged<Prediction> onPicked;
|
||||
final ValueChanged<String>? onSearchFailed;
|
||||
final SearchBarController searchBarController;
|
||||
final num? autocompleteOffset;
|
||||
final num? autocompleteRadius;
|
||||
final String? autocompleteLanguage;
|
||||
final List<String>? autocompleteTypes;
|
||||
final List<Component>? autocompleteComponents;
|
||||
final bool? strictbounds;
|
||||
final String? region;
|
||||
final GlobalKey appBarKey;
|
||||
final String? initialSearchString;
|
||||
final bool? searchForInitialValue;
|
||||
final bool? autocompleteOnTrailingWhitespace;
|
||||
|
||||
@override
|
||||
AutoCompleteSearchState createState() => AutoCompleteSearchState();
|
||||
}
|
||||
|
||||
class AutoCompleteSearchState extends State<AutoCompleteSearch> {
|
||||
TextEditingController controller = TextEditingController();
|
||||
FocusNode focus = FocusNode();
|
||||
OverlayEntry? overlayEntry;
|
||||
SearchProvider provider = SearchProvider();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialSearchString != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.text = widget.initialSearchString!;
|
||||
if (widget.searchForInitialValue!) {
|
||||
_onSearchInputChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
controller.addListener(_onSearchInputChange);
|
||||
focus.addListener(_onFocusChanged);
|
||||
|
||||
widget.searchBarController.attach(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.removeListener(_onSearchInputChange);
|
||||
controller.dispose();
|
||||
|
||||
focus.removeListener(_onFocusChanged);
|
||||
focus.dispose();
|
||||
_clearOverlay();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return !widget.hidden
|
||||
? ChangeNotifierProvider.value(
|
||||
value: provider,
|
||||
child: RoundedFrame(
|
||||
height: widget.height,
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.black54
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
elevation: 4.0,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
SizedBox(width: 10),
|
||||
Icon(Icons.search),
|
||||
SizedBox(width: 10),
|
||||
Expanded(child: _buildSearchTextField()),
|
||||
_buildTextClearIcon(),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
|
||||
Widget _buildSearchTextField() {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focus,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
border: InputBorder.none,
|
||||
errorBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
focusedErrorBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: widget.contentPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextClearIcon() {
|
||||
return Selector<SearchProvider, String>(
|
||||
selector: (_, provider) => provider.searchTerm,
|
||||
builder: (_, data, __) {
|
||||
if (data.length > 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: GestureDetector(
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
onTap: () {
|
||||
clearText();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(width: 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onSearchInputChange() {
|
||||
if (!mounted) return;
|
||||
this.provider.searchTerm = controller.text;
|
||||
|
||||
PlaceProvider provider = PlaceProvider.of(context, listen: false);
|
||||
|
||||
if (controller.text.isEmpty) {
|
||||
provider.debounceTimer?.cancel();
|
||||
_searchPlace(controller.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller.text.trim() == this.provider.prevSearchTerm.trim()) {
|
||||
provider.debounceTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!widget.autocompleteOnTrailingWhitespace! &&
|
||||
controller.text.substring(controller.text.length - 1) == " ") {
|
||||
provider.debounceTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider.debounceTimer?.isActive ?? false) {
|
||||
provider.debounceTimer!.cancel();
|
||||
}
|
||||
|
||||
provider.debounceTimer =
|
||||
Timer(Duration(milliseconds: widget.debounceMilliseconds!), () {
|
||||
_searchPlace(controller.text.trim());
|
||||
});
|
||||
}
|
||||
|
||||
_onFocusChanged() {
|
||||
PlaceProvider provider = PlaceProvider.of(context, listen: false);
|
||||
provider.isSearchBarFocused = focus.hasFocus;
|
||||
provider.debounceTimer?.cancel();
|
||||
provider.placeSearchingState = SearchingState.Idle;
|
||||
}
|
||||
|
||||
_searchPlace(String searchTerm) {
|
||||
this.provider.prevSearchTerm = searchTerm;
|
||||
|
||||
_clearOverlay();
|
||||
|
||||
if (searchTerm.length < 1) return;
|
||||
|
||||
_displayOverlay(_buildSearchingOverlay());
|
||||
|
||||
_performAutoCompleteSearch(searchTerm);
|
||||
}
|
||||
|
||||
_clearOverlay() {
|
||||
if (overlayEntry != null) {
|
||||
overlayEntry!.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
_displayOverlay(Widget overlayChild) {
|
||||
_clearOverlay();
|
||||
|
||||
final RenderBox? appBarRenderBox =
|
||||
widget.appBarKey.currentContext!.findRenderObject() as RenderBox?;
|
||||
final translation = appBarRenderBox?.getTransformTo(null).getTranslation();
|
||||
final Offset offset = translation != null
|
||||
? Offset(translation.x, translation.y)
|
||||
: Offset(0.0, 0.0);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
top: appBarRenderBox!.paintBounds.shift(offset).top +
|
||||
appBarRenderBox.size.height,
|
||||
left: screenWidth * 0.025,
|
||||
right: screenWidth * 0.025,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: overlayChild,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context)!.insert(overlayEntry!);
|
||||
}
|
||||
|
||||
Widget _buildSearchingOverlay() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
),
|
||||
SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.searchingText ?? "Searching...",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPredictionOverlay(List<Prediction> predictions) {
|
||||
return ListBody(
|
||||
children: predictions
|
||||
.map(
|
||||
(p) => PredictionTile(
|
||||
prediction: p,
|
||||
onTap: (selectedPrediction) {
|
||||
resetSearchBar();
|
||||
widget.onPicked(selectedPrediction);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
_performAutoCompleteSearch(String searchTerm) async {
|
||||
PlaceProvider provider = PlaceProvider.of(context, listen: false);
|
||||
|
||||
if (searchTerm.isNotEmpty) {
|
||||
final PlacesAutocompleteResponse response =
|
||||
await provider.places.autocomplete(
|
||||
searchTerm,
|
||||
sessionToken: widget.sessionToken,
|
||||
location: provider.currentPosition == null
|
||||
? null
|
||||
: Location(
|
||||
lat: provider.currentPosition!.latitude,
|
||||
lng: provider.currentPosition!.longitude),
|
||||
offset: widget.autocompleteOffset,
|
||||
radius: widget.autocompleteRadius,
|
||||
language: widget.autocompleteLanguage,
|
||||
types: widget.autocompleteTypes ?? const [],
|
||||
components: widget.autocompleteComponents ?? const [],
|
||||
strictbounds: widget.strictbounds ?? false,
|
||||
region: widget.region,
|
||||
);
|
||||
|
||||
if (response.errorMessage?.isNotEmpty == true ||
|
||||
response.status == "REQUEST_DENIED") {
|
||||
if (widget.onSearchFailed != null) {
|
||||
widget.onSearchFailed!(response.status);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_displayOverlay(_buildPredictionOverlay(response.predictions));
|
||||
}
|
||||
}
|
||||
|
||||
clearText() {
|
||||
provider.searchTerm = "";
|
||||
controller.clear();
|
||||
}
|
||||
|
||||
resetSearchBar() {
|
||||
clearText();
|
||||
focus.unfocus();
|
||||
}
|
||||
|
||||
clearOverlay() {
|
||||
_clearOverlay();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedPin extends StatefulWidget {
|
||||
AnimatedPin({
|
||||
Key? key,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
_AnimatedPinState createState() => _AnimatedPinState();
|
||||
}
|
||||
|
||||
class _AnimatedPinState extends State<AnimatedPin>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return JumpingContainer(controller: _controller, child: widget.child);
|
||||
}
|
||||
}
|
||||
|
||||
class JumpingContainer extends AnimatedWidget {
|
||||
const JumpingContainer({
|
||||
Key? key,
|
||||
required AnimationController controller,
|
||||
this.child,
|
||||
}) : super(key: key, listenable: controller);
|
||||
|
||||
final Widget? child;
|
||||
|
||||
Animation<double> get _progress => listenable as Animation<double>;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, -10 + _progress.value * 10),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/components/rounded_frame.dart';
|
||||
|
||||
class FloatingCard extends StatelessWidget {
|
||||
const FloatingCard({
|
||||
Key? key,
|
||||
this.topPosition,
|
||||
this.leftPosition,
|
||||
this.rightPosition,
|
||||
this.bottomPosition,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
this.elevation = 0.0,
|
||||
this.color,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final double? topPosition;
|
||||
final double? leftPosition;
|
||||
final double? bottomPosition;
|
||||
final double? rightPosition;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BorderRadius borderRadius;
|
||||
final double elevation;
|
||||
final Color? color;
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: topPosition,
|
||||
left: leftPosition,
|
||||
right: rightPosition,
|
||||
bottom: bottomPosition,
|
||||
child: RoundedFrame(
|
||||
width: width,
|
||||
height: height,
|
||||
borderRadius: borderRadius,
|
||||
elevation: elevation,
|
||||
color: color,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_webservice/places.dart';
|
||||
|
||||
class PredictionTile extends StatelessWidget {
|
||||
final Prediction prediction;
|
||||
final ValueChanged<Prediction>? onTap;
|
||||
|
||||
PredictionTile({required this.prediction, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.location_on),
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
children: _buildPredictionText(context),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap!(prediction);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<TextSpan> _buildPredictionText(BuildContext context) {
|
||||
final List<TextSpan> result = <TextSpan>[];
|
||||
final textColor = Colors.black;
|
||||
|
||||
if (prediction.matchedSubstrings.length > 0) {
|
||||
MatchedSubstring matchedSubString = prediction.matchedSubstrings[0];
|
||||
// There is no matched string at the beginning.
|
||||
if (matchedSubString.offset > 0) {
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: prediction.description
|
||||
?.substring(0, matchedSubString.offset as int?),
|
||||
style: TextStyle(
|
||||
color: textColor, fontSize: 16, fontWeight: FontWeight.w300),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Matched strings.
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: prediction.description?.substring(
|
||||
matchedSubString.offset as int,
|
||||
matchedSubString.offset + matchedSubString.length as int?),
|
||||
style: TextStyle(
|
||||
color: textColor, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
|
||||
// Other strings.
|
||||
if (matchedSubString.offset + matchedSubString.length <
|
||||
(prediction.description?.length ?? 0)) {
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: prediction.description?.substring(
|
||||
matchedSubString.offset + matchedSubString.length as int),
|
||||
style: TextStyle(
|
||||
color: textColor, fontSize: 16, fontWeight: FontWeight.w300),
|
||||
),
|
||||
);
|
||||
}
|
||||
// If there is no matched strings, but there are predicts. (Not sure if this happens though)
|
||||
} else {
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: prediction.description,
|
||||
style: TextStyle(
|
||||
color: textColor, fontSize: 16, fontWeight: FontWeight.w300),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RoundedFrame extends StatelessWidget {
|
||||
const RoundedFrame({
|
||||
Key? key,
|
||||
this.margin,
|
||||
this.padding,
|
||||
this.width,
|
||||
this.height,
|
||||
this.child,
|
||||
this.color,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
this.borderColor = Colors.transparent,
|
||||
this.elevation = 0.0,
|
||||
this.materialType,
|
||||
}) : super(key: key);
|
||||
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
final Color borderColor;
|
||||
final BorderRadius borderRadius;
|
||||
final double elevation;
|
||||
final MaterialType? materialType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
child: Material(
|
||||
type: color == Colors.transparent
|
||||
? MaterialType.transparency
|
||||
: materialType ?? MaterialType.canvas,
|
||||
color: color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius, side: BorderSide(color: borderColor)),
|
||||
elevation: elevation,
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/autocomplete_search.dart';
|
||||
|
||||
class SearchBarController extends ChangeNotifier {
|
||||
late AutoCompleteSearchState _autoCompleteSearch;
|
||||
|
||||
attach(AutoCompleteSearchState searchWidget) {
|
||||
_autoCompleteSearch = searchWidget;
|
||||
}
|
||||
|
||||
/// Just clears text.
|
||||
clear() {
|
||||
_autoCompleteSearch.clearText();
|
||||
}
|
||||
|
||||
/// Clear and remove focus (Dismiss keyboard)
|
||||
reset() {
|
||||
_autoCompleteSearch.resetSearchBar();
|
||||
}
|
||||
|
||||
clearOverlay() {
|
||||
_autoCompleteSearch.clearOverlay();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,612 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_maps_webservice/geocoding.dart';
|
||||
import 'package:google_maps_webservice/places.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/google_maps_place_picker.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/providers/place_provider.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/components/animated_pin.dart';
|
||||
|
||||
typedef SelectedPlaceWidgetBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
PickResult? selectedPlace,
|
||||
SearchingState state,
|
||||
bool isSearchBarFocused,
|
||||
);
|
||||
|
||||
typedef PinBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
PinState state,
|
||||
);
|
||||
|
||||
class GoogleMapPlacePicker extends StatelessWidget {
|
||||
const GoogleMapPlacePicker({
|
||||
Key? key,
|
||||
required this.initialTarget,
|
||||
required this.appBarKey,
|
||||
this.selectedPlaceWidgetBuilder,
|
||||
this.pinBuilder,
|
||||
this.onSearchFailed,
|
||||
this.onMoveStart,
|
||||
this.onMapCreated,
|
||||
this.debounceMilliseconds,
|
||||
this.enableMapTypeButton,
|
||||
this.enableMyLocationButton,
|
||||
this.onToggleMapType,
|
||||
this.onMyLocation,
|
||||
this.onPlacePicked,
|
||||
this.usePinPointingSearch,
|
||||
this.usePlaceDetailSearch,
|
||||
this.selectInitialPosition,
|
||||
this.language,
|
||||
this.pickArea,
|
||||
this.forceSearchOnZoomChanged,
|
||||
this.hidePlaceDetailsWhenDraggingPin,
|
||||
this.onCameraMoveStarted,
|
||||
this.onCameraMove,
|
||||
this.onCameraIdle,
|
||||
this.selectText,
|
||||
this.outsideOfPickAreaText,
|
||||
this.zoomGesturesEnabled = true,
|
||||
this.zoomControlsEnabled = false,
|
||||
this.fullMotion = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final LatLng initialTarget;
|
||||
final GlobalKey appBarKey;
|
||||
|
||||
final SelectedPlaceWidgetBuilder? selectedPlaceWidgetBuilder;
|
||||
final PinBuilder? pinBuilder;
|
||||
|
||||
final ValueChanged<String>? onSearchFailed;
|
||||
final VoidCallback? onMoveStart;
|
||||
final MapCreatedCallback? onMapCreated;
|
||||
final VoidCallback? onToggleMapType;
|
||||
final VoidCallback? onMyLocation;
|
||||
final ValueChanged<PickResult>? onPlacePicked;
|
||||
|
||||
final int? debounceMilliseconds;
|
||||
final bool? enableMapTypeButton;
|
||||
final bool? enableMyLocationButton;
|
||||
|
||||
final bool? usePinPointingSearch;
|
||||
final bool? usePlaceDetailSearch;
|
||||
|
||||
final bool? selectInitialPosition;
|
||||
|
||||
final String? language;
|
||||
final CircleArea? pickArea;
|
||||
|
||||
final bool? forceSearchOnZoomChanged;
|
||||
final bool? hidePlaceDetailsWhenDraggingPin;
|
||||
|
||||
/// GoogleMap pass-through events:
|
||||
final Function(PlaceProvider)? onCameraMoveStarted;
|
||||
final CameraPositionCallback? onCameraMove;
|
||||
final Function(PlaceProvider)? onCameraIdle;
|
||||
|
||||
// strings
|
||||
final String? selectText;
|
||||
final String? outsideOfPickAreaText;
|
||||
|
||||
/// Zoom feature toggle
|
||||
final bool zoomGesturesEnabled;
|
||||
final bool zoomControlsEnabled;
|
||||
|
||||
/// Use never scrollable scroll-view with maximum dimensions to prevent unnecessary re-rendering.
|
||||
final bool fullMotion;
|
||||
|
||||
_searchByCameraLocation(PlaceProvider provider) async {
|
||||
// We don't want to search location again if camera location is changed by zooming in/out.
|
||||
if (forceSearchOnZoomChanged == false &&
|
||||
provider.prevCameraPosition != null &&
|
||||
provider.prevCameraPosition!.target.latitude ==
|
||||
provider.cameraPosition!.target.latitude &&
|
||||
provider.prevCameraPosition!.target.longitude ==
|
||||
provider.cameraPosition!.target.longitude) {
|
||||
provider.placeSearchingState = SearchingState.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
provider.placeSearchingState = SearchingState.Searching;
|
||||
|
||||
final GeocodingResponse response =
|
||||
await provider.geocoding.searchByLocation(
|
||||
Location(
|
||||
lat: provider.cameraPosition!.target.latitude,
|
||||
lng: provider.cameraPosition!.target.longitude),
|
||||
language: language,
|
||||
);
|
||||
|
||||
if (response.errorMessage?.isNotEmpty == true ||
|
||||
response.status == "REQUEST_DENIED") {
|
||||
print("Camera Location Search Error: " + response.errorMessage!);
|
||||
if (onSearchFailed != null) {
|
||||
onSearchFailed!(response.status);
|
||||
}
|
||||
provider.placeSearchingState = SearchingState.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
if (usePlaceDetailSearch!) {
|
||||
final PlacesDetailsResponse detailResponse =
|
||||
await provider.places.getDetailsByPlaceId(
|
||||
response.results[0].placeId,
|
||||
language: language,
|
||||
);
|
||||
|
||||
if (detailResponse.errorMessage?.isNotEmpty == true ||
|
||||
detailResponse.status == "REQUEST_DENIED") {
|
||||
print("Fetching details by placeId Error: " +
|
||||
detailResponse.errorMessage!);
|
||||
if (onSearchFailed != null) {
|
||||
onSearchFailed!(detailResponse.status);
|
||||
}
|
||||
provider.placeSearchingState = SearchingState.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
provider.selectedPlace =
|
||||
PickResult.fromPlaceDetailResult(detailResponse.result);
|
||||
} else {
|
||||
provider.selectedPlace =
|
||||
PickResult.fromGeocodingResult(response.results[0]);
|
||||
}
|
||||
|
||||
provider.placeSearchingState = SearchingState.Idle;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
if (this.fullMotion)
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
_buildGoogleMap(context),
|
||||
_buildPin(),
|
||||
],
|
||||
))),
|
||||
if (!this.fullMotion) _buildGoogleMap(context),
|
||||
if (!this.fullMotion) _buildPin(),
|
||||
_buildFloatingCard(),
|
||||
_buildMapIcons(context),
|
||||
_buildZoomButtons()
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGoogleMapInner(PlaceProvider? provider, MapType mapType) {
|
||||
CameraPosition initialCameraPosition =
|
||||
CameraPosition(target: this.initialTarget, zoom: 15);
|
||||
return GoogleMap(
|
||||
zoomGesturesEnabled: this.zoomGesturesEnabled,
|
||||
zoomControlsEnabled:
|
||||
false, // we use our own implementation that supports iOS as well, see _buildZoomButtons()
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
mapType: mapType,
|
||||
myLocationEnabled: true,
|
||||
circles: pickArea != null && pickArea!.radius > 0
|
||||
? Set<Circle>.from([pickArea])
|
||||
: Set<Circle>(),
|
||||
onMapCreated: (GoogleMapController controller) {
|
||||
if (provider == null) return;
|
||||
provider.mapController = controller;
|
||||
provider.setCameraPosition(null);
|
||||
provider.pinState = PinState.Idle;
|
||||
|
||||
// When select initialPosition set to true.
|
||||
if (selectInitialPosition!) {
|
||||
provider.setCameraPosition(initialCameraPosition);
|
||||
_searchByCameraLocation(provider);
|
||||
}
|
||||
|
||||
if (onMapCreated != null) {
|
||||
onMapCreated!(controller);
|
||||
}
|
||||
},
|
||||
onCameraIdle: () {
|
||||
if (provider == null) return;
|
||||
if (provider.isAutoCompleteSearching) {
|
||||
provider.isAutoCompleteSearching = false;
|
||||
provider.pinState = PinState.Idle;
|
||||
provider.placeSearchingState = SearchingState.Idle;
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform search only if the setting is to true.
|
||||
if (usePinPointingSearch!) {
|
||||
// Search current camera location only if camera has moved (dragged) before.
|
||||
if (provider.pinState == PinState.Dragging) {
|
||||
// Cancel previous timer.
|
||||
if (provider.debounceTimer?.isActive ?? false) {
|
||||
provider.debounceTimer!.cancel();
|
||||
}
|
||||
provider.debounceTimer =
|
||||
Timer(Duration(milliseconds: debounceMilliseconds!), () {
|
||||
_searchByCameraLocation(provider);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
provider.pinState = PinState.Idle;
|
||||
|
||||
if (onCameraIdle != null) {
|
||||
onCameraIdle!(provider);
|
||||
}
|
||||
},
|
||||
onCameraMoveStarted: () {
|
||||
if (provider == null) return;
|
||||
if (onCameraMoveStarted != null) {
|
||||
onCameraMoveStarted!(provider);
|
||||
}
|
||||
|
||||
provider.setPrevCameraPosition(provider.cameraPosition);
|
||||
|
||||
// Cancel any other timer.
|
||||
provider.debounceTimer?.cancel();
|
||||
|
||||
// Update state, dismiss keyboard and clear text.
|
||||
provider.pinState = PinState.Dragging;
|
||||
|
||||
// Begins the search state if the hide details is enabled
|
||||
if (this.hidePlaceDetailsWhenDraggingPin!) {
|
||||
provider.placeSearchingState = SearchingState.Searching;
|
||||
}
|
||||
|
||||
onMoveStart!();
|
||||
},
|
||||
onCameraMove: (CameraPosition position) {
|
||||
if (provider == null) return;
|
||||
provider.setCameraPosition(position);
|
||||
if (onCameraMove != null) {
|
||||
onCameraMove!(position);
|
||||
}
|
||||
},
|
||||
// gestureRecognizers make it possible to navigate the map when it's a
|
||||
// child in a scroll view e.g ListView, SingleChildScrollView...
|
||||
gestureRecognizers: Set()
|
||||
..add(Factory<EagerGestureRecognizer>(() => EagerGestureRecognizer())),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGoogleMap(BuildContext context) {
|
||||
return Selector<PlaceProvider, MapType>(
|
||||
selector: (_, provider) => provider.mapType,
|
||||
builder: (_, data, __) => this._buildGoogleMapInner(
|
||||
PlaceProvider.of(context, listen: false), data));
|
||||
}
|
||||
|
||||
Widget _buildPin() {
|
||||
return Center(
|
||||
child: Selector<PlaceProvider, PinState>(
|
||||
selector: (_, provider) => provider.pinState,
|
||||
builder: (context, state, __) {
|
||||
if (pinBuilder == null) {
|
||||
return _defaultPinBuilder(context, state);
|
||||
} else {
|
||||
return Builder(
|
||||
builder: (builderContext) =>
|
||||
pinBuilder!(builderContext, state));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _defaultPinBuilder(BuildContext context, PinState state) {
|
||||
if (state == PinState.Preparing) {
|
||||
return Container();
|
||||
} else if (state == PinState.Idle) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(Icons.place, size: 36, color: Colors.red),
|
||||
SizedBox(height: 42),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
AnimatedPin(
|
||||
child: Icon(Icons.place, size: 36, color: Colors.red)),
|
||||
SizedBox(height: 42),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFloatingCard() {
|
||||
return Selector<PlaceProvider,
|
||||
Tuple4<PickResult?, SearchingState, bool, PinState>>(
|
||||
selector: (_, provider) => Tuple4(
|
||||
provider.selectedPlace,
|
||||
provider.placeSearchingState,
|
||||
provider.isSearchBarFocused,
|
||||
provider.pinState),
|
||||
builder: (context, data, __) {
|
||||
if ((data.item1 == null && data.item2 == SearchingState.Idle) ||
|
||||
data.item3 == true ||
|
||||
data.item4 == PinState.Dragging &&
|
||||
this.hidePlaceDetailsWhenDraggingPin!) {
|
||||
return Container();
|
||||
} else {
|
||||
if (selectedPlaceWidgetBuilder == null) {
|
||||
return _defaultPlaceWidgetBuilder(context, data.item1, data.item2);
|
||||
} else {
|
||||
return Builder(
|
||||
builder: (builderContext) => selectedPlaceWidgetBuilder!(
|
||||
builderContext, data.item1, data.item2, data.item3));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildZoomButtons() {
|
||||
return Selector<PlaceProvider, Tuple2<GoogleMapController?, LatLng?>>(
|
||||
selector: (_, provider) => new Tuple2<GoogleMapController?, LatLng?>(
|
||||
provider.mapController, provider.cameraPosition?.target),
|
||||
builder: (context, data, __) {
|
||||
if (!this.zoomControlsEnabled ||
|
||||
data.item1 == null ||
|
||||
data.item2 == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Positioned(
|
||||
bottom: 50,
|
||||
right: 10,
|
||||
child: Card(
|
||||
elevation: 4.0,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 100,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
double currentZoomLevel =
|
||||
await data.item1!.getZoomLevel();
|
||||
currentZoomLevel = currentZoomLevel + 2;
|
||||
data.item1!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: data.item2!,
|
||||
zoom: currentZoomLevel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 2),
|
||||
IconButton(
|
||||
icon: Icon(Icons.remove),
|
||||
onPressed: () async {
|
||||
double currentZoomLevel =
|
||||
await data.item1!.getZoomLevel();
|
||||
currentZoomLevel = currentZoomLevel - 2;
|
||||
if (currentZoomLevel < 0) currentZoomLevel = 0;
|
||||
data.item1!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: data.item2!,
|
||||
zoom: currentZoomLevel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _defaultPlaceWidgetBuilder(
|
||||
BuildContext context, PickResult? data, SearchingState state) {
|
||||
return FloatingCard(
|
||||
bottomPosition: MediaQuery.of(context).size.height * 0.1,
|
||||
leftPosition: MediaQuery.of(context).size.width * 0.15,
|
||||
rightPosition: MediaQuery.of(context).size.width * 0.15,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
elevation: 4.0,
|
||||
color: Theme.of(context).cardColor,
|
||||
child: state == SearchingState.Searching
|
||||
? _buildLoadingIndicator()
|
||||
: _buildSelectionDetails(context, data!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator() {
|
||||
return Container(
|
||||
height: 48,
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectionDetails(BuildContext context, PickResult result) {
|
||||
bool canBePicked = pickArea == null ||
|
||||
pickArea!.radius <= 0 ||
|
||||
Geolocator.distanceBetween(
|
||||
pickArea!.center.latitude,
|
||||
pickArea!.center.longitude,
|
||||
result.geometry!.location.lat,
|
||||
result.geometry!.location.lng) <=
|
||||
pickArea!.radius;
|
||||
MaterialStateColor buttonColor = MaterialStateColor.resolveWith(
|
||||
(states) => canBePicked ? Colors.lightGreen : Colors.red);
|
||||
return Container(
|
||||
margin: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
result.types!.length==1?
|
||||
result.formattedAddress!:result.name!+', '+result.formattedAddress!,
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
(canBePicked && (selectText?.isEmpty ?? true)) ||
|
||||
(!canBePicked && (outsideOfPickAreaText?.isEmpty ?? true))
|
||||
? SizedBox.fromSize(
|
||||
size: Size(56, 56), // button width and height
|
||||
child: ClipOval(
|
||||
child: Material(
|
||||
child: InkWell(
|
||||
overlayColor: buttonColor,
|
||||
onTap: () {
|
||||
if (canBePicked) {
|
||||
onPlacePicked!(result);
|
||||
}
|
||||
},
|
||||
child: Icon(
|
||||
canBePicked
|
||||
? Icons.check_sharp
|
||||
: Icons.app_blocking_sharp,
|
||||
color: buttonColor)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox.fromSize(
|
||||
size: Size(MediaQuery.of(context).size.width * 0.8,
|
||||
56), // button width and height
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: Material(
|
||||
child: InkWell(
|
||||
overlayColor: buttonColor,
|
||||
onTap: () {
|
||||
if (canBePicked) {
|
||||
onPlacePicked!(result);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
canBePicked
|
||||
? Icons.check_sharp
|
||||
: Icons.app_blocking_sharp,
|
||||
color: buttonColor),
|
||||
SizedBox.fromSize(size: new Size(10, 0)),
|
||||
Text(
|
||||
canBePicked
|
||||
? selectText!
|
||||
: outsideOfPickAreaText!,
|
||||
style: TextStyle(color: buttonColor))
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapIcons(BuildContext context) {
|
||||
if (appBarKey.currentContext == null) {
|
||||
return Container();
|
||||
}
|
||||
final RenderBox appBarRenderBox =
|
||||
appBarKey.currentContext!.findRenderObject() as RenderBox;
|
||||
return Positioned(
|
||||
top: appBarRenderBox.size.height,
|
||||
right: 15,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
enableMapTypeButton!
|
||||
? Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: RawMaterialButton(
|
||||
shape: CircleBorder(),
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.black54
|
||||
: Colors.white,
|
||||
elevation: 4.0,
|
||||
onPressed: onToggleMapType,
|
||||
child: Icon(Icons.layers),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(height: 10),
|
||||
enableMyLocationButton!
|
||||
? Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: RawMaterialButton(
|
||||
shape: CircleBorder(),
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.black54
|
||||
: Colors.white,
|
||||
elevation: 4.0,
|
||||
onPressed: onMyLocation,
|
||||
child: Icon(Icons.my_location),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class CircleArea extends Circle {
|
||||
CircleArea({
|
||||
required LatLng center,
|
||||
required double radius,
|
||||
Color? fillColor,
|
||||
Color? strokeColor,
|
||||
int strokeWidth = 2,
|
||||
}) : super(
|
||||
circleId: CircleId(Uuid().v4()),
|
||||
center: center,
|
||||
radius: radius,
|
||||
fillColor: fillColor ?? Colors.blue.withAlpha(32),
|
||||
strokeColor: strokeColor ?? Colors.blue.withAlpha(192),
|
||||
strokeWidth: strokeWidth,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import 'package:google_maps_webservice/geocoding.dart';
|
||||
import 'package:google_maps_webservice/places.dart';
|
||||
|
||||
class PickResult {
|
||||
PickResult({
|
||||
this.placeId,
|
||||
this.geometry,
|
||||
this.formattedAddress,
|
||||
this.types,
|
||||
this.addressComponents,
|
||||
this.adrAddress,
|
||||
this.formattedPhoneNumber,
|
||||
this.id,
|
||||
this.reference,
|
||||
this.icon,
|
||||
this.name,
|
||||
this.openingHours,
|
||||
this.photos,
|
||||
this.internationalPhoneNumber,
|
||||
this.priceLevel,
|
||||
this.rating,
|
||||
this.scope,
|
||||
this.url,
|
||||
this.vicinity,
|
||||
this.utcOffset,
|
||||
this.website,
|
||||
this.reviews,
|
||||
});
|
||||
|
||||
final String? placeId;
|
||||
final Geometry? geometry;
|
||||
final String? formattedAddress;
|
||||
final List<String>? types;
|
||||
final List<AddressComponent>? addressComponents;
|
||||
|
||||
// Below results will not be fetched if 'usePlaceDetailSearch' is set to false (Defaults to false).
|
||||
final String? adrAddress;
|
||||
final String? formattedPhoneNumber;
|
||||
final String? id;
|
||||
final String? reference;
|
||||
final String? icon;
|
||||
final String? name;
|
||||
final OpeningHoursDetail? openingHours;
|
||||
final List<Photo>? photos;
|
||||
final String? internationalPhoneNumber;
|
||||
final PriceLevel? priceLevel;
|
||||
final num? rating;
|
||||
final String? scope;
|
||||
final String? url;
|
||||
final String? vicinity;
|
||||
final num? utcOffset;
|
||||
final String? website;
|
||||
final List<Review>? reviews;
|
||||
|
||||
factory PickResult.fromGeocodingResult(GeocodingResult result) {
|
||||
return PickResult(
|
||||
placeId: result.placeId,
|
||||
geometry: result.geometry,
|
||||
formattedAddress: result.formattedAddress,
|
||||
types: result.types,
|
||||
addressComponents: result.addressComponents,
|
||||
);
|
||||
}
|
||||
|
||||
factory PickResult.fromPlaceDetailResult(PlaceDetails result) {
|
||||
return PickResult(
|
||||
placeId: result.placeId,
|
||||
geometry: result.geometry,
|
||||
formattedAddress: result.formattedAddress,
|
||||
types: result.types,
|
||||
addressComponents: result.addressComponents,
|
||||
adrAddress: result.adrAddress,
|
||||
formattedPhoneNumber: result.formattedPhoneNumber,
|
||||
id: result.id,
|
||||
reference: result.reference,
|
||||
icon: result.icon,
|
||||
name: result.name,
|
||||
openingHours: result.openingHours,
|
||||
photos: result.photos,
|
||||
internationalPhoneNumber: result.internationalPhoneNumber,
|
||||
priceLevel: result.priceLevel,
|
||||
rating: result.rating,
|
||||
scope: result.scope,
|
||||
url: result.url,
|
||||
vicinity: result.vicinity,
|
||||
utcOffset: result.utcOffset,
|
||||
website: result.website,
|
||||
reviews: result.reviews,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,535 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:google_api_headers/google_api_headers.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_maps_webservice/places.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/google_maps_place_picker.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/providers/place_provider.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/autocomplete_search.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/controllers/autocomplete_search_controller.dart';
|
||||
import 'package:supplier_new/google_maps_place_picker_mb/src/google_map_place_picker.dart';
|
||||
|
||||
typedef IntroModalWidgetBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Function? close,
|
||||
);
|
||||
|
||||
enum PinState { Preparing, Idle, Dragging }
|
||||
|
||||
enum SearchingState { Idle, Searching }
|
||||
|
||||
class PlacePicker extends StatefulWidget {
|
||||
const PlacePicker({
|
||||
Key? key,
|
||||
required this.apiKey,
|
||||
this.onPlacePicked,
|
||||
required this.initialPosition,
|
||||
this.useCurrentLocation,
|
||||
this.desiredLocationAccuracy = LocationAccuracy.high,
|
||||
this.onMapCreated,
|
||||
this.hintText,
|
||||
this.searchingText,
|
||||
this.selectText,
|
||||
this.outsideOfPickAreaText,
|
||||
this.onAutoCompleteFailed,
|
||||
this.onGeocodingSearchFailed,
|
||||
this.proxyBaseUrl,
|
||||
this.httpClient,
|
||||
this.selectedPlaceWidgetBuilder,
|
||||
this.pinBuilder,
|
||||
this.introModalWidgetBuilder,
|
||||
this.autoCompleteDebounceInMilliseconds = 500,
|
||||
this.cameraMoveDebounceInMilliseconds = 750,
|
||||
this.initialMapType = MapType.normal,
|
||||
this.enableMapTypeButton = true,
|
||||
this.enableMyLocationButton = true,
|
||||
this.myLocationButtonCooldown = 10,
|
||||
this.usePinPointingSearch = true,
|
||||
this.usePlaceDetailSearch = false,
|
||||
this.autocompleteOffset,
|
||||
this.autocompleteRadius,
|
||||
this.autocompleteLanguage,
|
||||
this.autocompleteComponents,
|
||||
this.autocompleteTypes,
|
||||
this.strictbounds,
|
||||
this.region,
|
||||
this.pickArea,
|
||||
this.selectInitialPosition = false,
|
||||
this.resizeToAvoidBottomInset = true,
|
||||
this.initialSearchString,
|
||||
this.searchForInitialValue = false,
|
||||
this.forceAndroidLocationManager = false,
|
||||
this.forceSearchOnZoomChanged = false,
|
||||
this.automaticallyImplyAppBarLeading = true,
|
||||
this.autocompleteOnTrailingWhitespace = false,
|
||||
this.hidePlaceDetailsWhenDraggingPin = true,
|
||||
this.onTapBack,
|
||||
this.onCameraMoveStarted,
|
||||
this.onCameraMove,
|
||||
this.onCameraIdle,
|
||||
this.onMapTypeChanged,
|
||||
this.zoomGesturesEnabled = true,
|
||||
this.zoomControlsEnabled = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final String apiKey;
|
||||
|
||||
final LatLng initialPosition;
|
||||
final bool? useCurrentLocation;
|
||||
final LocationAccuracy desiredLocationAccuracy;
|
||||
|
||||
final String? hintText;
|
||||
final String? searchingText;
|
||||
final String? selectText;
|
||||
final String? outsideOfPickAreaText;
|
||||
|
||||
final ValueChanged<String>? onAutoCompleteFailed;
|
||||
final ValueChanged<String>? onGeocodingSearchFailed;
|
||||
final int autoCompleteDebounceInMilliseconds;
|
||||
final int cameraMoveDebounceInMilliseconds;
|
||||
|
||||
final MapType initialMapType;
|
||||
final bool enableMapTypeButton;
|
||||
final bool enableMyLocationButton;
|
||||
final int myLocationButtonCooldown;
|
||||
|
||||
final bool usePinPointingSearch;
|
||||
final bool usePlaceDetailSearch;
|
||||
|
||||
final num? autocompleteOffset;
|
||||
final num? autocompleteRadius;
|
||||
final String? autocompleteLanguage;
|
||||
final List<String>? autocompleteTypes;
|
||||
final List<Component>? autocompleteComponents;
|
||||
final bool? strictbounds;
|
||||
final String? region;
|
||||
|
||||
/// If set the picker can only pick addresses in the given circle area.
|
||||
/// The section will be highlighted.
|
||||
final CircleArea? pickArea;
|
||||
|
||||
/// If true the [body] and the scaffold's floating widgets should size
|
||||
/// themselves to avoid the onscreen keyboard whose height is defined by the
|
||||
/// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property.
|
||||
///
|
||||
/// For example, if there is an onscreen keyboard displayed above the
|
||||
/// scaffold, the body can be resized to avoid overlapping the keyboard, which
|
||||
/// prevents widgets inside the body from being obscured by the keyboard.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool resizeToAvoidBottomInset;
|
||||
|
||||
final bool selectInitialPosition;
|
||||
|
||||
/// By using default setting of Place Picker, it will result result when user hits the select here button.
|
||||
///
|
||||
/// If you managed to use your own [selectedPlaceWidgetBuilder], then this WILL NOT be invoked, and you need use data which is
|
||||
/// being sent with [selectedPlaceWidgetBuilder].
|
||||
final ValueChanged<PickResult>? onPlacePicked;
|
||||
|
||||
/// optional - builds selected place's UI
|
||||
///
|
||||
/// It is provided by default if you leave it as a null.
|
||||
/// INPORTANT: If this is non-null, [onPlacePicked] will not be invoked, as there will be no default 'Select here' button.
|
||||
final SelectedPlaceWidgetBuilder? selectedPlaceWidgetBuilder;
|
||||
|
||||
/// optional - builds customized pin widget which indicates current pointing position.
|
||||
///
|
||||
/// It is provided by default if you leave it as a null.
|
||||
final PinBuilder? pinBuilder;
|
||||
|
||||
/// optional - builds customized introduction panel.
|
||||
///
|
||||
/// None is provided / the map is instantly accessible if you leave it as a null.
|
||||
final IntroModalWidgetBuilder? introModalWidgetBuilder;
|
||||
|
||||
/// optional - sets 'proxy' value in google_maps_webservice
|
||||
///
|
||||
/// In case of using a proxy the baseUrl can be set.
|
||||
/// The apiKey is not required in case the proxy sets it.
|
||||
/// (Not storing the apiKey in the app is good practice)
|
||||
final String? proxyBaseUrl;
|
||||
|
||||
/// optional - set 'client' value in google_maps_webservice
|
||||
///
|
||||
/// In case of using a proxy url that requires authentication
|
||||
/// or custom configuration
|
||||
final BaseClient? httpClient;
|
||||
|
||||
/// Initial value of autocomplete search
|
||||
final String? initialSearchString;
|
||||
|
||||
/// Whether to search for the initial value or not
|
||||
final bool searchForInitialValue;
|
||||
|
||||
/// On Android devices you can set [forceAndroidLocationManager]
|
||||
/// to true to force the plugin to use the [LocationManager] to determine the
|
||||
/// position instead of the [FusedLocationProviderClient]. On iOS this is ignored.
|
||||
final bool forceAndroidLocationManager;
|
||||
|
||||
/// Allow searching place when zoom has changed. By default searching is disabled when zoom has changed in order to prevent unwilling API usage.
|
||||
final bool forceSearchOnZoomChanged;
|
||||
|
||||
/// Whether to display appbar backbutton. Defaults to true.
|
||||
final bool automaticallyImplyAppBarLeading;
|
||||
|
||||
/// Will perform an autocomplete search, if set to true. Note that setting
|
||||
/// this to true, while providing a smoother UX experience, may cause
|
||||
/// additional unnecessary queries to the Places API.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool autocompleteOnTrailingWhitespace;
|
||||
|
||||
final bool hidePlaceDetailsWhenDraggingPin;
|
||||
|
||||
// Raised when clicking on the back arrow.
|
||||
// This will not listen for the system back button on Android devices.
|
||||
// If this is not set, but the back button is visible through automaticallyImplyLeading,
|
||||
// the Navigator will try to pop instead.
|
||||
final VoidCallback? onTapBack;
|
||||
|
||||
/// GoogleMap pass-through events:
|
||||
|
||||
/// Callback method for when the map is ready to be used.
|
||||
///
|
||||
/// Used to receive a [GoogleMapController] for this [GoogleMap].
|
||||
final MapCreatedCallback? onMapCreated;
|
||||
|
||||
/// Called when the camera starts moving.
|
||||
///
|
||||
/// This can be initiated by the following:
|
||||
/// 1. Non-gesture animation initiated in response to user actions.
|
||||
/// For example: zoom buttons, my location button, or marker clicks.
|
||||
/// 2. Programmatically initiated animation.
|
||||
/// 3. Camera motion initiated in response to user gestures on the map.
|
||||
/// For example: pan, tilt, pinch to zoom, or rotate.
|
||||
final Function(PlaceProvider)? onCameraMoveStarted;
|
||||
|
||||
/// Called repeatedly as the camera continues to move after an
|
||||
/// onCameraMoveStarted call.
|
||||
///
|
||||
/// This may be called as often as once every frame and should
|
||||
/// not perform expensive operations.
|
||||
final CameraPositionCallback? onCameraMove;
|
||||
|
||||
/// Called when camera movement has ended, there are no pending
|
||||
/// animations and the user has stopped interacting with the map.
|
||||
final Function(PlaceProvider)? onCameraIdle;
|
||||
|
||||
/// Called when the map type has been changed.
|
||||
final Function(MapType)? onMapTypeChanged;
|
||||
|
||||
/// Allow user to make visible the zoom button & toggle on & off zoom gestures
|
||||
final bool zoomGesturesEnabled;
|
||||
final bool zoomControlsEnabled;
|
||||
|
||||
@override
|
||||
_PlacePickerState createState() => _PlacePickerState();
|
||||
}
|
||||
|
||||
class _PlacePickerState extends State<PlacePicker> {
|
||||
GlobalKey appBarKey = GlobalKey();
|
||||
late final Future<PlaceProvider> _futureProvider;
|
||||
PlaceProvider? provider;
|
||||
SearchBarController searchBarController = SearchBarController();
|
||||
bool showIntroModal = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureProvider = _initPlaceProvider();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchBarController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<PlaceProvider> _initPlaceProvider() async {
|
||||
final headers = await const GoogleApiHeaders().getHeaders();
|
||||
final provider = PlaceProvider(
|
||||
widget.apiKey,
|
||||
widget.proxyBaseUrl,
|
||||
widget.httpClient,
|
||||
headers,
|
||||
);
|
||||
provider.sessionToken = const Uuid().v4();
|
||||
provider.desiredAccuracy = widget.desiredLocationAccuracy;
|
||||
provider.setMapType(widget.initialMapType);
|
||||
if (widget.useCurrentLocation != null && widget.useCurrentLocation!) {
|
||||
await provider.updateCurrentLocation(widget.forceAndroidLocationManager);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () {
|
||||
searchBarController.clearOverlay();
|
||||
return Future.value(true);
|
||||
},
|
||||
child: FutureBuilder<PlaceProvider>(
|
||||
future: _futureProvider,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasData) {
|
||||
provider = snapshot.data;
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<PlaceProvider>.value(value: provider!),
|
||||
],
|
||||
child: Stack(children: [
|
||||
Scaffold(
|
||||
key: ValueKey<int>(provider.hashCode),
|
||||
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
key: appBarKey,
|
||||
automaticallyImplyLeading: false,
|
||||
iconTheme: Theme.of(context).iconTheme,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
titleSpacing: 0.0,
|
||||
title: _buildSearchBar(context),
|
||||
),
|
||||
body: _buildMapWithLocation(),
|
||||
),
|
||||
_buildIntroModal(context),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
final children = <Widget>[];
|
||||
if (snapshot.hasError) {
|
||||
children.addAll([
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text('Error: ${snapshot.error}'),
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
children.add(CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildSearchBar(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
widget.automaticallyImplyAppBarLeading || widget.onTapBack != null
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
if (!showIntroModal ||
|
||||
widget.introModalWidgetBuilder == null) {
|
||||
if (widget.onTapBack != null) {
|
||||
widget.onTapBack!();
|
||||
return;
|
||||
}
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back,
|
||||
),
|
||||
color: Colors.black.withAlpha(128),
|
||||
padding: EdgeInsets.zero)
|
||||
: SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: AutoCompleteSearch(
|
||||
appBarKey: appBarKey,
|
||||
searchBarController: searchBarController,
|
||||
sessionToken: provider!.sessionToken,
|
||||
hintText: widget.hintText,
|
||||
searchingText: widget.searchingText,
|
||||
debounceMilliseconds: widget.autoCompleteDebounceInMilliseconds,
|
||||
onPicked: (prediction) {
|
||||
_pickPrediction(prediction);
|
||||
},
|
||||
onSearchFailed: (status) {
|
||||
if (widget.onAutoCompleteFailed != null) {
|
||||
widget.onAutoCompleteFailed!(status);
|
||||
}
|
||||
},
|
||||
autocompleteOffset: widget.autocompleteOffset,
|
||||
autocompleteRadius: widget.autocompleteRadius,
|
||||
autocompleteLanguage: widget.autocompleteLanguage,
|
||||
autocompleteComponents: widget.autocompleteComponents,
|
||||
autocompleteTypes: widget.autocompleteTypes,
|
||||
strictbounds: widget.strictbounds,
|
||||
region: widget.region,
|
||||
initialSearchString: widget.initialSearchString,
|
||||
searchForInitialValue: widget.searchForInitialValue,
|
||||
autocompleteOnTrailingWhitespace:
|
||||
widget.autocompleteOnTrailingWhitespace),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_pickPrediction(Prediction prediction) async {
|
||||
provider!.placeSearchingState = SearchingState.Searching;
|
||||
|
||||
final PlacesDetailsResponse response =
|
||||
await provider!.places.getDetailsByPlaceId(
|
||||
prediction.placeId!,
|
||||
sessionToken: provider!.sessionToken,
|
||||
language: widget.autocompleteLanguage,
|
||||
);
|
||||
|
||||
if (response.errorMessage?.isNotEmpty == true ||
|
||||
response.status == "REQUEST_DENIED") {
|
||||
if (widget.onAutoCompleteFailed != null) {
|
||||
widget.onAutoCompleteFailed!(response.status);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
provider!.selectedPlace = PickResult.fromPlaceDetailResult(response.result);
|
||||
|
||||
// Prevents searching again by camera movement.
|
||||
provider!.isAutoCompleteSearching = true;
|
||||
|
||||
await _moveTo(provider!.selectedPlace!.geometry!.location.lat,
|
||||
provider!.selectedPlace!.geometry!.location.lng);
|
||||
|
||||
provider!.placeSearchingState = SearchingState.Idle;
|
||||
}
|
||||
|
||||
_moveTo(double latitude, double longitude) async {
|
||||
GoogleMapController? controller = provider!.mapController;
|
||||
if (controller == null) return;
|
||||
|
||||
await controller.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: LatLng(latitude, longitude),
|
||||
zoom: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_moveToCurrentPosition() async {
|
||||
if (provider!.currentPosition != null) {
|
||||
await _moveTo(provider!.currentPosition!.latitude,
|
||||
provider!.currentPosition!.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMapWithLocation() {
|
||||
if (provider!.currentPosition == null) {
|
||||
return _buildMap(widget.initialPosition);
|
||||
}
|
||||
return _buildMap(LatLng(provider!.currentPosition!.latitude,
|
||||
provider!.currentPosition!.longitude));
|
||||
}
|
||||
|
||||
Widget _buildMap(LatLng initialTarget) {
|
||||
return GoogleMapPlacePicker(
|
||||
fullMotion: !widget.resizeToAvoidBottomInset,
|
||||
initialTarget: initialTarget,
|
||||
appBarKey: appBarKey,
|
||||
selectedPlaceWidgetBuilder: widget.selectedPlaceWidgetBuilder,
|
||||
pinBuilder: widget.pinBuilder,
|
||||
onSearchFailed: widget.onGeocodingSearchFailed,
|
||||
debounceMilliseconds: widget.cameraMoveDebounceInMilliseconds,
|
||||
enableMapTypeButton: widget.enableMapTypeButton,
|
||||
enableMyLocationButton: widget.enableMyLocationButton,
|
||||
usePinPointingSearch: widget.usePinPointingSearch,
|
||||
usePlaceDetailSearch: widget.usePlaceDetailSearch,
|
||||
onMapCreated: widget.onMapCreated,
|
||||
selectInitialPosition: widget.selectInitialPosition,
|
||||
language: widget.autocompleteLanguage,
|
||||
pickArea: widget.pickArea,
|
||||
forceSearchOnZoomChanged: widget.forceSearchOnZoomChanged,
|
||||
hidePlaceDetailsWhenDraggingPin: widget.hidePlaceDetailsWhenDraggingPin,
|
||||
selectText: widget.selectText,
|
||||
outsideOfPickAreaText: widget.outsideOfPickAreaText,
|
||||
onToggleMapType: () {
|
||||
provider!.switchMapType();
|
||||
if (widget.onMapTypeChanged != null) {
|
||||
widget.onMapTypeChanged!(provider!.mapType);
|
||||
}
|
||||
},
|
||||
onMyLocation: () async {
|
||||
// Prevent to click many times in short period.
|
||||
if (provider!.isOnUpdateLocationCooldown == false) {
|
||||
provider!.isOnUpdateLocationCooldown = true;
|
||||
Timer(Duration(seconds: widget.myLocationButtonCooldown), () {
|
||||
provider!.isOnUpdateLocationCooldown = false;
|
||||
});
|
||||
await provider!
|
||||
.updateCurrentLocation(widget.forceAndroidLocationManager);
|
||||
await _moveToCurrentPosition();
|
||||
}
|
||||
},
|
||||
onMoveStart: () {
|
||||
searchBarController.reset();
|
||||
},
|
||||
onPlacePicked: widget.onPlacePicked,
|
||||
onCameraMoveStarted: widget.onCameraMoveStarted,
|
||||
onCameraMove: widget.onCameraMove,
|
||||
onCameraIdle: widget.onCameraIdle,
|
||||
zoomGesturesEnabled: widget.zoomGesturesEnabled,
|
||||
zoomControlsEnabled: widget.zoomControlsEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIntroModal(BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return showIntroModal && widget.introModalWidgetBuilder != null
|
||||
? Stack(children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: Material(
|
||||
type: MaterialType.canvas,
|
||||
color: Color.fromARGB(128, 0, 0, 0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
child: ClipRect(),
|
||||
),
|
||||
),
|
||||
widget.introModalWidgetBuilder!(context, () {
|
||||
setState(() {
|
||||
showIntroModal = false;
|
||||
});
|
||||
})
|
||||
])
|
||||
: Container();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'helper.dart';
|
||||
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
static const Color primaryColor = Color(0xffed1846);
|
||||
static const Color secondaryColor = Color(0xff5bcb84);
|
||||
static const Color tColor = Color(0xfff087ca);
|
||||
static const Color rColor = Color(0xfff08c87);
|
||||
static const Color statusColorPending = Color(0xfff8c942);
|
||||
static const Color statusColorInProgress = Color(0xff587add);
|
||||
static const Color statusColorConfirm = Color(0xff30d300);
|
||||
static const Color pageBgColor = Color(0xFFF6F6F6);
|
||||
static const Color grey100Color = Color(0xFFEEEEEE);
|
||||
static const Color grey200Color = Color(0xFFEEEEEE);
|
||||
static const Color grey300Color = Color(0xFFE0E0E0);
|
||||
static const Color grey400Color = Color(0xFFBDBDBD);
|
||||
static const Color grey500Color = Color(0xFF9E9E9E);
|
||||
static const Color grey600Color = Color(0xFF757575);
|
||||
static const Color grey700Color = Color(0xFF616161);
|
||||
static const Color grey800Color = Color(0xFF424242);
|
||||
static const Color grey900Color = Color(0xFF212121);
|
||||
static const Color errorColor = Color(0xFFD50000);
|
||||
static const Color error100Color = Color(0xffFF5252);
|
||||
static const Color mainBgColor = Color(0xfff7f7f7);
|
||||
static const Color logoColor = Color(0xfff1ea0c);
|
||||
|
||||
static MaterialColor primaryMaterialColor = getSwatch(primaryColor);
|
||||
static MaterialColor errorMaterialColor = getSwatch(errorColor);
|
||||
static MaterialColor tableRowMaterialColor = getSwatch(grey500Color);
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
class AppImages {
|
||||
static const String pickupMarker = "assets/images/pickup_marker.png";
|
||||
static const String dropMarker = "assets/images/drop_marker.png";
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
||||
|
||||
class AppSizes {
|
||||
// get height and width from getX
|
||||
static final double deviceHeight = Get.height;
|
||||
static final double deviceWidth = Get.width;
|
||||
|
||||
static const int height1060 = 1060;
|
||||
static const int height880 = 880;
|
||||
static const int height740 = 740;
|
||||
static const int height490 = 490;
|
||||
|
||||
static const int width1060 = 1060;
|
||||
static const int width880 = 880;
|
||||
static const int width740 = 740;
|
||||
static const int width490 = 490;
|
||||
|
||||
static const int screen720x1280 = 490;
|
||||
|
||||
static final double mapPinSize = (deviceWidth * window.devicePixelRatio);
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
MaterialColor getSwatch(Color color) {
|
||||
final hslColor = HSLColor.fromColor(color);
|
||||
final lightness = hslColor.lightness;
|
||||
|
||||
/// if [500] is the default color, there are at LEAST five
|
||||
/// steps below [500]. (i.e. 400, 300, 200, 100, 50.) A
|
||||
/// divisor of 5 would mean [50] is a lightness of 1.0 or
|
||||
/// a color of #ffffff. A value of six would be near white
|
||||
/// but not quite.
|
||||
const lowDivisor = 6;
|
||||
|
||||
/// if [500] is the default color, there are at LEAST four
|
||||
/// steps above [500]. A divisor of 4 would mean [900] is
|
||||
/// a lightness of 0.0 or color of #000000
|
||||
const highDivisor = 5;
|
||||
|
||||
final lowStep = (1.0 - lightness) / lowDivisor;
|
||||
final highStep = lightness / highDivisor;
|
||||
|
||||
return MaterialColor(color.value, {
|
||||
50: (hslColor.withLightness(lightness + (lowStep * 5))).toColor(),
|
||||
100: (hslColor.withLightness(lightness + (lowStep * 4))).toColor(),
|
||||
200: (hslColor.withLightness(lightness + (lowStep * 3))).toColor(),
|
||||
300: (hslColor.withLightness(lightness + (lowStep * 2))).toColor(),
|
||||
400: (hslColor.withLightness(lightness + lowStep)).toColor(),
|
||||
500: (hslColor.withLightness(lightness)).toColor(),
|
||||
600: (hslColor.withLightness(lightness - highStep)).toColor(),
|
||||
700: (hslColor.withLightness(lightness - (highStep * 2))).toColor(),
|
||||
800: (hslColor.withLightness(lightness - (highStep * 3))).toColor(),
|
||||
900: (hslColor.withLightness(lightness - (highStep * 4))).toColor(),
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:location/location.dart';
|
||||
|
||||
import 'app_images.dart';
|
||||
import 'app_sizes.dart';
|
||||
import 'permission_alert.dart';
|
||||
|
||||
class LocationController<T> extends GetxController {
|
||||
Location location = Location();
|
||||
|
||||
// final Rx<LatLng?> locationPosition = const LatLng(0.0, 0.0).obs;
|
||||
/*final Rx<LatLng?> locationPosition =
|
||||
const LatLng(12.90618717, 77.5844983).obs;*/
|
||||
final Rx<LatLng?> locationPosition =
|
||||
const LatLng(0, 0).obs;
|
||||
|
||||
bool locationServiceActive = true;
|
||||
|
||||
BitmapDescriptor? pickupMarker;
|
||||
BitmapDescriptor? dropMarker;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
|
||||
await _getBytesFromAsset(AppImages.pickupMarker, AppSizes.mapPinSize * 0.1);
|
||||
await _getBytesFromAsset(AppImages.dropMarker, AppSizes.mapPinSize * 0.05);
|
||||
|
||||
super.onInit();
|
||||
refreshToLiveLocation();
|
||||
}
|
||||
|
||||
Future<void> _getBytesFromAsset(String path, double size) async {
|
||||
ByteData data = await rootBundle.load(path);
|
||||
ui.Codec codec = await ui.instantiateImageCodec(
|
||||
data.buffer.asUint8List(),
|
||||
targetWidth: size.toInt(),
|
||||
allowUpscaling: true,
|
||||
);
|
||||
ui.FrameInfo fi = await codec.getNextFrame();
|
||||
if (path == AppImages.pickupMarker) {
|
||||
pickupMarker = BitmapDescriptor.fromBytes(
|
||||
(await fi.image.toByteData(format: ui.ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List());
|
||||
} else if (path == AppImages.dropMarker) {
|
||||
dropMarker = BitmapDescriptor.fromBytes(
|
||||
(await fi.image.toByteData(format: ui.ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List());
|
||||
} else {}
|
||||
}
|
||||
|
||||
refreshToLiveLocation() async {
|
||||
log("initiating");
|
||||
bool serviceEnabled;
|
||||
|
||||
PermissionStatus permissionGranted;
|
||||
|
||||
serviceEnabled = await location.serviceEnabled();
|
||||
|
||||
if (!serviceEnabled) {
|
||||
serviceEnabled = await location.requestService();
|
||||
locationPosition.value = null;
|
||||
return;
|
||||
}
|
||||
log("permission check");
|
||||
permissionGranted = await location.hasPermission();
|
||||
|
||||
if (permissionGranted == PermissionStatus.denied) {
|
||||
permissionGranted = await location.requestPermission();
|
||||
if (permissionGranted != PermissionStatus.granted) {
|
||||
showPermissionAlertDialog(
|
||||
requestMsg:
|
||||
"Location access needed. Go to Android settings, tap App permissions and tap Allow.",
|
||||
barrierDismissible: false,
|
||||
);
|
||||
} else {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
interval: 2000,
|
||||
distanceFilter: 2);
|
||||
|
||||
location.onLocationChanged.listen((LocationData currentLocation) async {
|
||||
var lat = currentLocation.latitude;
|
||||
var long = currentLocation.longitude;
|
||||
if (lat != null && long != null) {
|
||||
locationPosition.value = LatLng(
|
||||
lat,
|
||||
long,
|
||||
);
|
||||
log("live location ${locationPosition.value}");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
interval: 2000,
|
||||
distanceFilter: 2);
|
||||
|
||||
location.onLocationChanged.listen((LocationData currentLocation) async {
|
||||
var lat = currentLocation.latitude;
|
||||
var long = currentLocation.longitude;
|
||||
if (lat != null && long != null) {
|
||||
locationPosition.value = LatLng(
|
||||
lat,
|
||||
long,
|
||||
);
|
||||
log("live location ${locationPosition.value}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'helper.dart';
|
||||
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
static const Color primaryColor = Color(0xffed1846);
|
||||
static const Color secondaryColor = Color(0xff5bcb84);
|
||||
static const Color tColor = Color(0xfff087ca);
|
||||
static const Color rColor = Color(0xfff08c87);
|
||||
static const Color statusColorPending = Color(0xfff8c942);
|
||||
static const Color statusColorInProgress = Color(0xff587add);
|
||||
static const Color statusColorConfirm = Color(0xff30d300);
|
||||
static const Color pageBgColor = Color(0xFFF6F6F6);
|
||||
static const Color grey100Color = Color(0xFFEEEEEE);
|
||||
static const Color grey200Color = Color(0xFFEEEEEE);
|
||||
static const Color grey300Color = Color(0xFFE0E0E0);
|
||||
static const Color grey400Color = Color(0xFFBDBDBD);
|
||||
static const Color grey500Color = Color(0xFF9E9E9E);
|
||||
static const Color grey600Color = Color(0xFF757575);
|
||||
static const Color grey700Color = Color(0xFF616161);
|
||||
static const Color grey800Color = Color(0xFF424242);
|
||||
static const Color grey900Color = Color(0xFF212121);
|
||||
static const Color errorColor = Color(0xFFD50000);
|
||||
static const Color error100Color = Color(0xffFF5252);
|
||||
static const Color mainBgColor = Color(0xfff7f7f7);
|
||||
static const Color logoColor = Color(0xfff1ea0c);
|
||||
|
||||
static MaterialColor primaryMaterialColor = getSwatch(primaryColor);
|
||||
static MaterialColor errorMaterialColor = getSwatch(errorColor);
|
||||
static MaterialColor tableRowMaterialColor = getSwatch(grey500Color);
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
class AppImages {
|
||||
static const String pickupMarker = "assets/images/pickup_marker.png";
|
||||
static const String dropMarker = "assets/images/drop_marker.png";
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
||||
|
||||
class AppSizes {
|
||||
// get height and width from getX
|
||||
static final double deviceHeight = Get.height;
|
||||
static final double deviceWidth = Get.width;
|
||||
|
||||
static const int height1060 = 1060;
|
||||
static const int height880 = 880;
|
||||
static const int height740 = 740;
|
||||
static const int height490 = 490;
|
||||
|
||||
static const int width1060 = 1060;
|
||||
static const int width880 = 880;
|
||||
static const int width740 = 740;
|
||||
static const int width490 = 490;
|
||||
|
||||
static const int screen720x1280 = 490;
|
||||
|
||||
static final double mapPinSize = (deviceWidth * window.devicePixelRatio);
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
MaterialColor getSwatch(Color color) {
|
||||
final hslColor = HSLColor.fromColor(color);
|
||||
final lightness = hslColor.lightness;
|
||||
|
||||
/// if [500] is the default color, there are at LEAST five
|
||||
/// steps below [500]. (i.e. 400, 300, 200, 100, 50.) A
|
||||
/// divisor of 5 would mean [50] is a lightness of 1.0 or
|
||||
/// a color of #ffffff. A value of six would be near white
|
||||
/// but not quite.
|
||||
const lowDivisor = 6;
|
||||
|
||||
/// if [500] is the default color, there are at LEAST four
|
||||
/// steps above [500]. A divisor of 4 would mean [900] is
|
||||
/// a lightness of 0.0 or color of #000000
|
||||
const highDivisor = 5;
|
||||
|
||||
final lowStep = (1.0 - lightness) / lowDivisor;
|
||||
final highStep = lightness / highDivisor;
|
||||
|
||||
return MaterialColor(color.value, {
|
||||
50: (hslColor.withLightness(lightness + (lowStep * 5))).toColor(),
|
||||
100: (hslColor.withLightness(lightness + (lowStep * 4))).toColor(),
|
||||
200: (hslColor.withLightness(lightness + (lowStep * 3))).toColor(),
|
||||
300: (hslColor.withLightness(lightness + (lowStep * 2))).toColor(),
|
||||
400: (hslColor.withLightness(lightness + lowStep)).toColor(),
|
||||
500: (hslColor.withLightness(lightness)).toColor(),
|
||||
600: (hslColor.withLightness(lightness - highStep)).toColor(),
|
||||
700: (hslColor.withLightness(lightness - (highStep * 2))).toColor(),
|
||||
800: (hslColor.withLightness(lightness - (highStep * 3))).toColor(),
|
||||
900: (hslColor.withLightness(lightness - (highStep * 4))).toColor(),
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:location/location.dart';
|
||||
|
||||
import 'app_images.dart';
|
||||
import 'app_sizes.dart';
|
||||
import 'permission_alert.dart';
|
||||
|
||||
class LocationController<T> extends GetxController {
|
||||
Location location = Location();
|
||||
|
||||
// final Rx<LatLng?> locationPosition = const LatLng(0.0, 0.0).obs;
|
||||
/*final Rx<LatLng?> locationPosition =
|
||||
const LatLng(12.90618717, 77.5844983).obs;*/
|
||||
final Rx<LatLng?> locationPosition =
|
||||
const LatLng(0, 0).obs;
|
||||
|
||||
bool locationServiceActive = true;
|
||||
|
||||
BitmapDescriptor? pickupMarker;
|
||||
BitmapDescriptor? dropMarker;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
|
||||
await _getBytesFromAsset(AppImages.pickupMarker, AppSizes.mapPinSize * 0.1);
|
||||
await _getBytesFromAsset(AppImages.dropMarker, AppSizes.mapPinSize * 0.05);
|
||||
|
||||
super.onInit();
|
||||
refreshToLiveLocation();
|
||||
}
|
||||
|
||||
Future<void> _getBytesFromAsset(String path, double size) async {
|
||||
ByteData data = await rootBundle.load(path);
|
||||
ui.Codec codec = await ui.instantiateImageCodec(
|
||||
data.buffer.asUint8List(),
|
||||
targetWidth: size.toInt(),
|
||||
allowUpscaling: true,
|
||||
);
|
||||
ui.FrameInfo fi = await codec.getNextFrame();
|
||||
if (path == AppImages.pickupMarker) {
|
||||
pickupMarker = BitmapDescriptor.fromBytes(
|
||||
(await fi.image.toByteData(format: ui.ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List());
|
||||
} else if (path == AppImages.dropMarker) {
|
||||
dropMarker = BitmapDescriptor.fromBytes(
|
||||
(await fi.image.toByteData(format: ui.ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List());
|
||||
} else {}
|
||||
}
|
||||
|
||||
refreshToLiveLocation() async {
|
||||
log("initiating");
|
||||
bool serviceEnabled;
|
||||
|
||||
PermissionStatus permissionGranted;
|
||||
|
||||
serviceEnabled = await location.serviceEnabled();
|
||||
|
||||
if (!serviceEnabled) {
|
||||
serviceEnabled = await location.requestService();
|
||||
locationPosition.value = null;
|
||||
return;
|
||||
}
|
||||
log("permission check");
|
||||
permissionGranted = await location.hasPermission();
|
||||
|
||||
if (permissionGranted == PermissionStatus.denied) {
|
||||
permissionGranted = await location.requestPermission();
|
||||
if (permissionGranted != PermissionStatus.granted) {
|
||||
showPermissionAlertDialog(
|
||||
requestMsg:
|
||||
"Location access needed. Go to Android settings, tap App permissions and tap Allow.",
|
||||
barrierDismissible: false,
|
||||
);
|
||||
} else {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
interval: 2000,
|
||||
distanceFilter: 2);
|
||||
|
||||
location.onLocationChanged.listen((LocationData currentLocation) async {
|
||||
var lat = currentLocation.latitude;
|
||||
var long = currentLocation.longitude;
|
||||
if (lat != null && long != null) {
|
||||
locationPosition.value = LatLng(
|
||||
lat,
|
||||
long,
|
||||
);
|
||||
log("live location ${locationPosition.value}");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
interval: 2000,
|
||||
distanceFilter: 2);
|
||||
|
||||
location.onLocationChanged.listen((LocationData currentLocation) async {
|
||||
var lat = currentLocation.latitude;
|
||||
var long = currentLocation.longitude;
|
||||
if (lat != null && long != null) {
|
||||
locationPosition.value = LatLng(
|
||||
lat,
|
||||
long,
|
||||
);
|
||||
log("live location ${locationPosition.value}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,252 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:location/location.dart';
|
||||
import 'location_controller.dart';
|
||||
|
||||
|
||||
class OrderTrackingPage extends StatefulWidget {
|
||||
var lat;
|
||||
var lng;
|
||||
var d_lat;
|
||||
var d_lng;
|
||||
var u_address;
|
||||
|
||||
OrderTrackingPage({
|
||||
this.lat,
|
||||
this.lng,
|
||||
this.d_lat,
|
||||
this.d_lng,
|
||||
this.u_address
|
||||
|
||||
});
|
||||
|
||||
@override
|
||||
OrderTrackingPageState createState() => OrderTrackingPageState();
|
||||
}
|
||||
|
||||
class OrderTrackingPageState extends State<OrderTrackingPage> {
|
||||
final Completer<GoogleMapController> mapController = Completer();
|
||||
PolylinePoints polylinePoints = PolylinePoints();
|
||||
double latitude=0;
|
||||
double longitude=0;
|
||||
double d_latitude=0;
|
||||
double d_longitude=0;
|
||||
String u_address = '';
|
||||
LocationData? currentLocation;
|
||||
|
||||
|
||||
String googleAPiKey ="AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM";
|
||||
|
||||
Set<Marker> markers = {};
|
||||
Map<PolylineId, Polyline> polylines = {};
|
||||
|
||||
late LatLng startLocation ;
|
||||
late LatLng user_location;
|
||||
LocationController locationController = Get.put(LocationController());
|
||||
double distance = 0.0;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
latitude=widget.lat;
|
||||
longitude=widget.lng;
|
||||
d_latitude=widget.d_lat;
|
||||
d_longitude=widget.d_lng;
|
||||
u_address=widget.u_address;
|
||||
|
||||
|
||||
user_location = LatLng(widget.lat,widget.lng);
|
||||
startLocation = LatLng(widget.d_lat,widget.d_lng);
|
||||
|
||||
LatLng delivery_Location = LatLng(widget.d_lat,widget.d_lng);
|
||||
|
||||
//LatLng endLocation = LatLng(17.4968,78.3614);
|
||||
|
||||
ever<LatLng?>(locationController.locationPosition, (value) {
|
||||
if (value != null) {
|
||||
// log("${value.latitude} ${value.longitude}");
|
||||
var latitude = value.latitude;
|
||||
var longitude = value.longitude;
|
||||
startLocation = LatLng(widget.d_lat, widget.d_lng);
|
||||
getDirections(user_location);
|
||||
}
|
||||
});
|
||||
getDirections(user_location); //fetch direction polylines from Google API
|
||||
}
|
||||
|
||||
getDirections(user_location) async {
|
||||
markers.clear();
|
||||
|
||||
markers.add(Marker(
|
||||
markerId: MarkerId(startLocation.toString()),
|
||||
position: startLocation,
|
||||
infoWindow: const InfoWindow(
|
||||
title: 'Starting Point',
|
||||
snippet: 'Start Marker',
|
||||
),
|
||||
icon: locationController.pickupMarker ?? BitmapDescriptor.defaultMarker,
|
||||
));
|
||||
|
||||
markers.add(Marker(
|
||||
markerId: MarkerId(user_location.toString()),
|
||||
position: user_location, //position of marker
|
||||
infoWindow: const InfoWindow(
|
||||
title: 'Destination Point ',
|
||||
snippet: 'Destination Marker',
|
||||
),
|
||||
icon: locationController.dropMarker ?? BitmapDescriptor.defaultMarker,
|
||||
));
|
||||
|
||||
List<LatLng> polylineCoordinates = [];
|
||||
|
||||
PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
|
||||
googleAPiKey,
|
||||
PointLatLng(startLocation.latitude, startLocation.longitude),
|
||||
PointLatLng(user_location.latitude, user_location.longitude),
|
||||
travelMode: TravelMode.driving,
|
||||
);
|
||||
|
||||
if (result.points.isNotEmpty) {
|
||||
for (var point in result.points) {
|
||||
polylineCoordinates.add(LatLng(point.latitude, point.longitude));
|
||||
}
|
||||
} else {
|
||||
// log(result.errorMessage ?? "Something went wrong");
|
||||
}
|
||||
|
||||
//polulineCoordinates is the List of longitute and latidtude.
|
||||
double totalDistance = 0;
|
||||
for(var i = 0; i < polylineCoordinates.length-1; i++){
|
||||
totalDistance += calculateDistance(
|
||||
polylineCoordinates[i].latitude,
|
||||
polylineCoordinates[i].longitude,
|
||||
polylineCoordinates[i+1].latitude,
|
||||
polylineCoordinates[i+1].longitude);
|
||||
}
|
||||
print(totalDistance);
|
||||
|
||||
setState(() {
|
||||
distance = totalDistance;
|
||||
});
|
||||
|
||||
addPolyLine(polylineCoordinates);
|
||||
}
|
||||
|
||||
addPolyLine(List<LatLng> polylineCoordinates) async {
|
||||
PolylineId id = const PolylineId("poly");
|
||||
Polyline polyline = Polyline(
|
||||
polylineId: id,
|
||||
color: Colors.deepPurpleAccent,
|
||||
points: polylineCoordinates,
|
||||
width:8,
|
||||
);
|
||||
polylines[id] =polyline;
|
||||
|
||||
var position = CameraPosition(
|
||||
target: LatLng(latitude,longitude),
|
||||
zoom: 16);
|
||||
|
||||
final GoogleMapController controller = await mapController.future;
|
||||
controller.animateCamera(CameraUpdate.newCameraPosition(position));
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
||||
double calculateDistance(lat1, lon1, lat2, lon2){
|
||||
var p = 0.017453292519943295;
|
||||
var a = 0.5 - cos((lat2 - lat1) * p)/2 +
|
||||
cos(lat1 * p) * cos(lat2 * p) *
|
||||
(1 - cos((lon2 - lon1) * p))/2;
|
||||
return 12742 * asin(sqrt(a));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
GoogleMap(
|
||||
//Map widget from google_maps_flutter package
|
||||
zoomGesturesEnabled: true,
|
||||
//enable Zoom in, out on map
|
||||
initialCameraPosition: CameraPosition(
|
||||
//innital position in map
|
||||
target: startLocation, //initial position
|
||||
zoom: 8.0, //initial zoom level
|
||||
),
|
||||
|
||||
markers: markers,
|
||||
//markers to show on map
|
||||
polylines: Set<Polyline>.of(polylines.values),
|
||||
//polylines
|
||||
mapType: MapType.normal,
|
||||
//map type
|
||||
onMapCreated: (controller) {
|
||||
//method called when map is created
|
||||
if (!mapController.isCompleted) {
|
||||
mapController.complete(controller);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 95,
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 50,
|
||||
child: Container(
|
||||
child: Card(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text("Total Distance: " + distance.toStringAsFixed(2) + " KM",
|
||||
style: TextStyle(fontSize: 20, fontWeight:FontWeight.bold))
|
||||
),
|
||||
)
|
||||
)),
|
||||
/* const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 80,
|
||||
left: 0,
|
||||
child: Container(
|
||||
child: Card(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
|
||||
child: Text("User Address: " + u_address.toString() ,
|
||||
style: TextStyle(fontSize: 15, fontWeight:FontWeight.bold,overflow: TextOverflow.ellipsis))
|
||||
),
|
||||
)
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
left: 50,
|
||||
child: Container(
|
||||
child: Card(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text("Total DistanceD: " + distance.toStringAsFixed(2) + " KM",
|
||||
style: TextStyle(fontSize: 20, fontWeight:FontWeight.bold))
|
||||
),
|
||||
)
|
||||
)),*/
|
||||
],
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
enum PaddingType { symmetric, only }
|
||||
@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/get_navigation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import 'app_colors.dart';
|
||||
import 'primary_button.dart';
|
||||
import 'primary_text.dart';
|
||||
|
||||
void showPermissionAlertDialog({
|
||||
String title = "Need Permission",
|
||||
required String requestMsg,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
middleText: "",
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.only(top: 30, bottom: 30.0),
|
||||
radius: 10,
|
||||
barrierDismissible: barrierDismissible,
|
||||
titlePadding: const EdgeInsets.only(top: 15),
|
||||
titleStyle: const TextStyle(
|
||||
color: AppColors.grey900Color,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
/*cancel: PrimaryButton(
|
||||
title: "DISMISS",
|
||||
onPressed: () {},
|
||||
textSize: AppSizes.font_13,
|
||||
bgColor: AppColors.grey500Color,
|
||||
),*/
|
||||
confirm: PrimaryButton(
|
||||
title: "GO TO SETTINGS",
|
||||
onPressed: () {
|
||||
openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
textSize: 13,
|
||||
),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: PrimaryText(
|
||||
requestMsg,
|
||||
textAlign: TextAlign.center,
|
||||
fontColor: AppColors.grey800Color,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_colors.dart';
|
||||
import 'primary_text.dart';
|
||||
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final double verticalPadding;
|
||||
final bool textAllCaps;
|
||||
final double textSize;
|
||||
final FontWeight textWeight;
|
||||
final Color textColor;
|
||||
final TextDecoration textDecoration;
|
||||
final int letterSpacing;
|
||||
final bool isResponsive;
|
||||
final Color bgColor;
|
||||
final double horizontalPadding;
|
||||
|
||||
const PrimaryButton({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
this.verticalPadding = 10.0,
|
||||
this.textAllCaps = true,
|
||||
this.textSize = 14,
|
||||
this.textWeight = FontWeight.w500,
|
||||
this.textColor = Colors.white,
|
||||
this.textDecoration = TextDecoration.none,
|
||||
this.letterSpacing = 1,
|
||||
this.isResponsive = true,
|
||||
this.bgColor = AppColors.primaryColor,
|
||||
this.horizontalPadding = 25.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
color: bgColor,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: verticalPadding,
|
||||
horizontal: horizontalPadding,
|
||||
),
|
||||
child: PrimaryText(
|
||||
textAllCaps ? title.toUpperCase() : title,
|
||||
// title.toUpperCase(),
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: textSize,
|
||||
fontWeight: textWeight,
|
||||
fontColor: textColor,
|
||||
textDecoration: textDecoration,
|
||||
isResponsive: isResponsive,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_sizes.dart';
|
||||
import 'padding_type_enum.dart';
|
||||
|
||||
|
||||
class PrimaryText extends StatelessWidget {
|
||||
final String text;
|
||||
final Color? fontColor;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final double horizontalPadding;
|
||||
final double verticalPadding;
|
||||
final TextAlign textAlign;
|
||||
final bool isResponsive;
|
||||
final TextDecoration? textDecoration;
|
||||
final PaddingType paddingType;
|
||||
final double leftPadding;
|
||||
final double rightPadding;
|
||||
final double lineHeight;
|
||||
final FontStyle fontStyle;
|
||||
final TextOverflow textOverflow;
|
||||
final bool textAllCaps;
|
||||
final double letterSpacing;
|
||||
|
||||
const PrimaryText(
|
||||
this.text, {
|
||||
Key? key,
|
||||
this.fontColor = Colors.black,
|
||||
this.fontSize = 16,
|
||||
this.fontWeight = FontWeight.w600,
|
||||
this.horizontalPadding = 0.0,
|
||||
this.verticalPadding = 0.0,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.isResponsive = true,
|
||||
this.textDecoration,
|
||||
this.paddingType = PaddingType.symmetric,
|
||||
this.leftPadding = 0.0,
|
||||
this.rightPadding = 0.0,
|
||||
this.lineHeight = 1.5,
|
||||
this.fontStyle = FontStyle.normal,
|
||||
this.textOverflow = TextOverflow.visible,
|
||||
this.textAllCaps = false,
|
||||
this.letterSpacing = 0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: paddingType == PaddingType.symmetric
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
)
|
||||
: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
right: rightPadding,
|
||||
),
|
||||
child: Text(
|
||||
textAllCaps ? text.toUpperCase() : text,
|
||||
style: TextStyle(
|
||||
fontSize: responsiveTextSize(),
|
||||
fontWeight: fontWeight,
|
||||
color: fontColor,
|
||||
decoration: textDecoration,
|
||||
height: lineHeight,
|
||||
fontStyle: fontStyle,
|
||||
letterSpacing: letterSpacing,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double responsiveTextSize() {
|
||||
if (isResponsive) {
|
||||
if (AppSizes.deviceHeight < AppSizes.height490) {
|
||||
return fontSize - 3;
|
||||
} else if (AppSizes.deviceHeight < AppSizes.height740) {
|
||||
return fontSize - 2;
|
||||
} else if (AppSizes.deviceHeight < AppSizes.height880) {
|
||||
return fontSize - 1;
|
||||
}
|
||||
}
|
||||
return fontSize;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
enum PaddingType { symmetric, only }
|
||||
@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/get_navigation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import 'app_colors.dart';
|
||||
import 'primary_button.dart';
|
||||
import 'primary_text.dart';
|
||||
|
||||
void showPermissionAlertDialog({
|
||||
String title = "Need Permission",
|
||||
required String requestMsg,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
middleText: "",
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.only(top: 30, bottom: 30.0),
|
||||
radius: 10,
|
||||
barrierDismissible: barrierDismissible,
|
||||
titlePadding: const EdgeInsets.only(top: 15),
|
||||
titleStyle: const TextStyle(
|
||||
color: AppColors.grey900Color,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
/*cancel: PrimaryButton(
|
||||
title: "DISMISS",
|
||||
onPressed: () {},
|
||||
textSize: AppSizes.font_13,
|
||||
bgColor: AppColors.grey500Color,
|
||||
),*/
|
||||
confirm: PrimaryButton(
|
||||
title: "GO TO SETTINGS",
|
||||
onPressed: () {
|
||||
openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
textSize: 13,
|
||||
),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: PrimaryText(
|
||||
requestMsg,
|
||||
textAlign: TextAlign.center,
|
||||
fontColor: AppColors.grey800Color,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_colors.dart';
|
||||
import 'primary_text.dart';
|
||||
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final double verticalPadding;
|
||||
final bool textAllCaps;
|
||||
final double textSize;
|
||||
final FontWeight textWeight;
|
||||
final Color textColor;
|
||||
final TextDecoration textDecoration;
|
||||
final int letterSpacing;
|
||||
final bool isResponsive;
|
||||
final Color bgColor;
|
||||
final double horizontalPadding;
|
||||
|
||||
const PrimaryButton({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
this.verticalPadding = 10.0,
|
||||
this.textAllCaps = true,
|
||||
this.textSize = 14,
|
||||
this.textWeight = FontWeight.w500,
|
||||
this.textColor = Colors.white,
|
||||
this.textDecoration = TextDecoration.none,
|
||||
this.letterSpacing = 1,
|
||||
this.isResponsive = true,
|
||||
this.bgColor = AppColors.primaryColor,
|
||||
this.horizontalPadding = 25.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
color: bgColor,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: verticalPadding,
|
||||
horizontal: horizontalPadding,
|
||||
),
|
||||
child: PrimaryText(
|
||||
textAllCaps ? title.toUpperCase() : title,
|
||||
// title.toUpperCase(),
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: textSize,
|
||||
fontWeight: textWeight,
|
||||
fontColor: textColor,
|
||||
textDecoration: textDecoration,
|
||||
isResponsive: isResponsive,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_sizes.dart';
|
||||
import 'padding_type_enum.dart';
|
||||
|
||||
|
||||
class PrimaryText extends StatelessWidget {
|
||||
final String text;
|
||||
final Color? fontColor;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final double horizontalPadding;
|
||||
final double verticalPadding;
|
||||
final TextAlign textAlign;
|
||||
final bool isResponsive;
|
||||
final TextDecoration? textDecoration;
|
||||
final PaddingType paddingType;
|
||||
final double leftPadding;
|
||||
final double rightPadding;
|
||||
final double lineHeight;
|
||||
final FontStyle fontStyle;
|
||||
final TextOverflow textOverflow;
|
||||
final bool textAllCaps;
|
||||
final double letterSpacing;
|
||||
|
||||
const PrimaryText(
|
||||
this.text, {
|
||||
Key? key,
|
||||
this.fontColor = Colors.black,
|
||||
this.fontSize = 16,
|
||||
this.fontWeight = FontWeight.w600,
|
||||
this.horizontalPadding = 0.0,
|
||||
this.verticalPadding = 0.0,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.isResponsive = true,
|
||||
this.textDecoration,
|
||||
this.paddingType = PaddingType.symmetric,
|
||||
this.leftPadding = 0.0,
|
||||
this.rightPadding = 0.0,
|
||||
this.lineHeight = 1.5,
|
||||
this.fontStyle = FontStyle.normal,
|
||||
this.textOverflow = TextOverflow.visible,
|
||||
this.textAllCaps = false,
|
||||
this.letterSpacing = 0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: paddingType == PaddingType.symmetric
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
)
|
||||
: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
right: rightPadding,
|
||||
),
|
||||
child: Text(
|
||||
textAllCaps ? text.toUpperCase() : text,
|
||||
style: TextStyle(
|
||||
fontSize: responsiveTextSize(),
|
||||
fontWeight: fontWeight,
|
||||
color: fontColor,
|
||||
decoration: textDecoration,
|
||||
height: lineHeight,
|
||||
fontStyle: fontStyle,
|
||||
letterSpacing: letterSpacing,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double responsiveTextSize() {
|
||||
if (isResponsive) {
|
||||
if (AppSizes.deviceHeight < AppSizes.height490) {
|
||||
return fontSize - 3;
|
||||
} else if (AppSizes.deviceHeight < AppSizes.height740) {
|
||||
return fontSize - 2;
|
||||
} else if (AppSizes.deviceHeight < AppSizes.height880) {
|
||||
return fontSize - 1;
|
||||
}
|
||||
}
|
||||
return fontSize;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,234 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supplier_new/orders/edit_order_requests.dart';
|
||||
|
||||
class AcceptOrderRequests extends StatefulWidget {
|
||||
var order;
|
||||
AcceptOrderRequests({this.order});
|
||||
@override
|
||||
State<AcceptOrderRequests> createState() => _AcceptOrderRequestsState();
|
||||
}
|
||||
|
||||
class _AcceptOrderRequestsState extends State<AcceptOrderRequests> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline, color: Colors.black),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// 🔹 Top Card with Image
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(
|
||||
"images/building.png", // replace with network image
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
/// Status Chip
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
"New",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
/// Title + Location + Distance
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
"Club Kohinoor",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
"Banjara Hills, Hyderabad • 5.5 Km",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
/// 🔹 Order Details
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"ORDER DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_detailRow("Tanker Price", "₹ ${widget.order.quoted_amount}"),
|
||||
_detailRow("Water Type", " ${widget.order.type_of_water}"),
|
||||
_detailRow("Date of Delivery", "${widget.order.time}"),
|
||||
_detailRow("Capacity", "${widget.order.capacity}"),
|
||||
_detailRow("Time of Delivery", "${widget.order.averageTime}"),
|
||||
_detailRow("Quantity", "${widget.order.quantity}"),
|
||||
_detailRow("Advance", "10%"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
/// 🔹 Additional Details
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(
|
||||
"ADDITIONAL DETAILS\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, "
|
||||
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut "
|
||||
"aliquip ex ea commodo consequat.",
|
||||
style: TextStyle(fontSize: 13, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
/// 🔹 Payment Summary
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"PAYMENT SUMMARY",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_detailRow("Tanker Price", "₹ 1,500"),
|
||||
_detailRow("Advance", "10%"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 80), // space for bottom buttons
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// 🔹 Bottom Action Buttons
|
||||
bottomNavigationBar: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => EditOrderRequests(order: widget.order,advance:"10%" ,),
|
||||
),
|
||||
);
|
||||
|
||||
},
|
||||
child: const Text("Edit Order"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
side: const BorderSide(color: Colors.red),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: const Text("Reject"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: const Text("Accept"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Helper widget for rows
|
||||
Widget _detailRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(fontSize: 13, color: Colors.grey)),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,216 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditOrderRequests extends StatefulWidget {
|
||||
var order;
|
||||
String? advance;
|
||||
EditOrderRequests({this.order,this.advance});
|
||||
|
||||
@override
|
||||
State<EditOrderRequests> createState() => _EditOrderRequestsState();
|
||||
}
|
||||
|
||||
class _EditOrderRequestsState extends State<EditOrderRequests> {
|
||||
final TextEditingController tankerPriceController = TextEditingController();
|
||||
final TextEditingController waterTypeController = TextEditingController();
|
||||
final TextEditingController capacityController = TextEditingController();
|
||||
final TextEditingController timeController = TextEditingController();
|
||||
final TextEditingController quantityController = TextEditingController();
|
||||
final TextEditingController advanceController = TextEditingController();
|
||||
final TextEditingController dateController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tankerPriceController.text='${widget.order.quoted_amount}';
|
||||
waterTypeController.text='${widget.order.type_of_water}';
|
||||
quantityController.text='${widget.order.quantity}';
|
||||
capacityController.text='${widget.order.capacity}';
|
||||
timeController.text='${widget.order.averageTime}';
|
||||
dateController.text='${widget.order.time}';
|
||||
advanceController.text='${widget.advance}';
|
||||
// Update summary in real-time as user types
|
||||
tankerPriceController.addListener(() => setState(() {}));
|
||||
capacityController.addListener(() => setState(() {}));
|
||||
quantityController.addListener(() => setState(() {}));
|
||||
dateController.addListener(() => setState(() {}));
|
||||
timeController.addListener(() => setState(() {}));
|
||||
waterTypeController.addListener(() => setState(() {}));
|
||||
advanceController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int tankerPrice = int.tryParse(tankerPriceController.text) ?? 0;
|
||||
int updatedQuantity=int.tryParse(quantityController.text) ?? 0;
|
||||
String updatedCapacity=capacityController.text ?? '';
|
||||
int totalPrice = tankerPrice * updatedQuantity;
|
||||
double advancePercent =
|
||||
double.tryParse(advanceController.text.replaceAll('%', '')) ?? 0;
|
||||
int advancePayable = (totalPrice * (advancePercent / 100)).round();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
"Edit Order",
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w600),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔹 Club Info Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Chip(
|
||||
label: Text("New",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600)),
|
||||
backgroundColor: Color(0xFFEAF3FF),
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
Text("Club Kohinoor",
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
SizedBox(height: 4),
|
||||
Text("Banjara Hills, Hyderabad",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text("5.5 Km", style: TextStyle(color: Colors.black54)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("ORDER DETAILS",
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||
const SizedBox(height: 12),
|
||||
// 🔹 Two in a row
|
||||
_twoFields(tankerPriceController, "Tanker Price", null, null),
|
||||
_twoFields(capacityController, "Capacity", quantityController, "Quantity"),
|
||||
_twoFields(waterTypeController, "Water Type",advanceController, "Advance"),
|
||||
_twoFields(dateController, "Date",timeController, "Time Of Delivery"),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const Text("UPDATED PAYMENT SUMMARY",
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||
const SizedBox(height: 12),
|
||||
_summaryRow("Tanker Price", "₹ $tankerPrice"),
|
||||
_summaryRow("Quantity", " $updatedQuantity"),
|
||||
_summaryRow("Capacity", "$updatedCapacity"),
|
||||
_summaryRow("Total Price", "₹ $totalPrice"),
|
||||
const Divider(),
|
||||
_summaryRow("Advance", advanceController.text),
|
||||
_summaryRow("Advance Payable", "₹ $advancePayable"),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.deepPurple,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
// 🔹 Collect updated values
|
||||
print("Tanker Price: ${tankerPriceController.text}");
|
||||
print("Water Type: ${waterTypeController.text}");
|
||||
// Save logic here
|
||||
},
|
||||
child: const Text("Send ➤"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Two fields side by side
|
||||
Widget _twoFields(TextEditingController? controller1, String? label1,
|
||||
TextEditingController? controller2, String? label2) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _textField(controller1, label1),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (controller2 != null)
|
||||
Expanded(
|
||||
child: _textField(controller2, label2),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Custom text field
|
||||
Widget _textField(TextEditingController? controller, String? label) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Summary row
|
||||
Widget _summaryRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(color: Colors.black54)),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, color: Colors.black)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
|
||||
import 'package:supplier_new/common/settings.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class OrderRequestsModel {
|
||||
String building_name = '';
|
||||
String address = '';
|
||||
String type_of_water = '';
|
||||
String capacity = '';
|
||||
String quantity = '';
|
||||
String time = '';
|
||||
String averageTime = '';
|
||||
String quoted_amount = '';
|
||||
String displayAddress='';
|
||||
double lat=0;
|
||||
double lng=0;
|
||||
double distanceInMeters=0;
|
||||
|
||||
OrderRequestsModel();
|
||||
|
||||
factory OrderRequestsModel.fromJson(Map<String, dynamic> json){
|
||||
OrderRequestsModel rtvm = new OrderRequestsModel();
|
||||
|
||||
rtvm.building_name = json['customer_details']['buildingName'] ?? '';
|
||||
rtvm.address = json['customer_details']['profile']['address1'] ?? '';
|
||||
rtvm.type_of_water = json['type_of_water'] ?? '';
|
||||
rtvm.capacity = json['capacity'] ?? '';
|
||||
rtvm.quantity = json['quantity']?? '';
|
||||
rtvm.averageTime = json['time'] ?? '';
|
||||
rtvm.time = json['my_supplier_entry']['time'] ?? '';
|
||||
rtvm.quoted_amount = json['my_supplier_entry']['quoted_amount'].toString() ?? '';
|
||||
// Split and trim
|
||||
List<String> parts = rtvm.address.split(',').map((e) => e.trim()).toList();
|
||||
|
||||
// Usually, the locality is the part before the main city (Hyderabad)displayAddress = "";
|
||||
if (parts.length >= 2) {
|
||||
rtvm.displayAddress = parts[parts.length -4]; // "Banjara Hills"
|
||||
}
|
||||
/* rtvm.distanceInMeters = double.parse((Geolocator.distanceBetween(
|
||||
rtvm.lat,
|
||||
rtvm.lng,
|
||||
AppSettings.supplierLatitude,
|
||||
AppSettings.supplierLongitude
|
||||
) / 1000).toStringAsFixed(2));*/
|
||||
|
||||
return rtvm;
|
||||
}
|
||||
Map<String, dynamic> toJson() => {
|
||||
"boreName":this.building_name,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_place_picker_mb/google_maps_place_picker.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
|
||||
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
|
||||
|
||||
class GooglePlacePicker extends StatefulWidget {
|
||||
GooglePlacePicker({Key? key}) : super(key: key);
|
||||
static final kInitialPosition = LatLng(17.4167312, 78.4519109);
|
||||
|
||||
final GoogleMapsFlutterPlatform mapsImplementation =
|
||||
GoogleMapsFlutterPlatform.instance;
|
||||
|
||||
@override
|
||||
State<GooglePlacePicker> createState() => _GooglePlacePickerState();
|
||||
}
|
||||
|
||||
class _GooglePlacePickerState extends State<GooglePlacePicker> {
|
||||
PickResult? selectedPlace;
|
||||
bool _showPlacePickerInContainer = false;
|
||||
bool _showGoogleMapInContainer = false;
|
||||
|
||||
bool _mapsInitialized = false;
|
||||
String _mapsRenderer = "latest";
|
||||
|
||||
void initRenderer() {
|
||||
if (_mapsInitialized) return;
|
||||
if (widget.mapsImplementation is GoogleMapsFlutterAndroid) {
|
||||
switch (_mapsRenderer) {
|
||||
case "legacy":
|
||||
(widget.mapsImplementation as GoogleMapsFlutterAndroid)
|
||||
.initializeWithRenderer(AndroidMapRenderer.legacy);
|
||||
break;
|
||||
case "latest":
|
||||
(widget.mapsImplementation as GoogleMapsFlutterAndroid)
|
||||
.initializeWithRenderer(AndroidMapRenderer.latest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_mapsInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Google Map Place Picker Demo"),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!_mapsInitialized &&
|
||||
widget.mapsImplementation
|
||||
is GoogleMapsFlutterAndroid) ...[
|
||||
Switch(
|
||||
value: (widget.mapsImplementation
|
||||
as GoogleMapsFlutterAndroid)
|
||||
.useAndroidViewSurface,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
(widget.mapsImplementation
|
||||
as GoogleMapsFlutterAndroid)
|
||||
.useAndroidViewSurface = value;
|
||||
});
|
||||
}),
|
||||
Text("Hybrid Composition"),
|
||||
]
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!_mapsInitialized &&
|
||||
widget.mapsImplementation
|
||||
is GoogleMapsFlutterAndroid) ...[
|
||||
Text("Renderer: "),
|
||||
Radio(
|
||||
groupValue: _mapsRenderer,
|
||||
value: "auto",
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_mapsRenderer = "auto";
|
||||
});
|
||||
}),
|
||||
Text("Auto"),
|
||||
Radio(
|
||||
groupValue: _mapsRenderer,
|
||||
value: "legacy",
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_mapsRenderer = "legacy";
|
||||
});
|
||||
}),
|
||||
Text("Legacy"),
|
||||
Radio(
|
||||
groupValue: _mapsRenderer,
|
||||
value: "latest",
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_mapsRenderer = "latest";
|
||||
});
|
||||
}),
|
||||
Text("Latest"),
|
||||
]
|
||||
],
|
||||
),
|
||||
!_showPlacePickerInContainer
|
||||
? ElevatedButton(
|
||||
child: Text("Load Place Picker"),
|
||||
onPressed: () {
|
||||
initRenderer();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return PlacePicker(
|
||||
resizeToAvoidBottomInset:
|
||||
false, // only works in page mode, less flickery
|
||||
apiKey: Platform.isAndroid
|
||||
? "AIzaSyB4FhQFeC9JMeIadUwIO6HkoD4UFG6N-HM"
|
||||
: "IOS API KEY",
|
||||
hintText: "Find a place ...",
|
||||
searchingText: "Please wait ...",
|
||||
selectText: "Select place",
|
||||
outsideOfPickAreaText: "Place not in area",
|
||||
initialPosition: GooglePlacePicker.kInitialPosition,
|
||||
useCurrentLocation: true,
|
||||
selectInitialPosition: true,
|
||||
usePinPointingSearch: true,
|
||||
usePlaceDetailSearch: true,
|
||||
zoomGesturesEnabled: true,
|
||||
zoomControlsEnabled: true,
|
||||
onMapCreated: (GoogleMapController controller) {
|
||||
print("Map created");
|
||||
},
|
||||
onPlacePicked: (PickResult result) {
|
||||
print(
|
||||
"Place picked: ${result.formattedAddress}");
|
||||
setState(() {
|
||||
selectedPlace = result;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
onMapTypeChanged: (MapType mapType) {
|
||||
print(
|
||||
"Map type changed to ${mapType.toString()}");
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(),
|
||||
!_showPlacePickerInContainer
|
||||
? ElevatedButton(
|
||||
child: Text("Load Place Picker in Container"),
|
||||
onPressed: () {
|
||||
initRenderer();
|
||||
setState(() {
|
||||
_showPlacePickerInContainer = true;
|
||||
});
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.75,
|
||||
height: MediaQuery.of(context).size.height * 0.35,
|
||||
child: PlacePicker(
|
||||
//forceAndroidLocationManager: true,
|
||||
apiKey: Platform.isAndroid
|
||||
? "AIzaSyB4FhQFeC9JMeIadUwIO6HkoD4UFG6N-HM"
|
||||
: "IOS API KEY",
|
||||
hintText: "Find a place ...",
|
||||
searchingText: "Please wait ...",
|
||||
selectText: "Select place",
|
||||
initialPosition: GooglePlacePicker.kInitialPosition,
|
||||
useCurrentLocation: true,
|
||||
selectInitialPosition: true,
|
||||
usePinPointingSearch: true,
|
||||
usePlaceDetailSearch: true,
|
||||
zoomGesturesEnabled: true,
|
||||
zoomControlsEnabled: true,
|
||||
onPlacePicked: (PickResult result) {
|
||||
setState(() {
|
||||
selectedPlace = result;
|
||||
_showPlacePickerInContainer = false;
|
||||
});
|
||||
},
|
||||
onTapBack: () {
|
||||
setState(() {
|
||||
_showPlacePickerInContainer = false;
|
||||
});
|
||||
})),
|
||||
if (selectedPlace != null) ...[
|
||||
Text(selectedPlace!.formattedAddress!),
|
||||
Text("(lat: " +
|
||||
selectedPlace!.geometry!.location.lat.toString() +
|
||||
", lng: " +
|
||||
selectedPlace!.geometry!.location.lng.toString() +
|
||||
")"),
|
||||
],
|
||||
// #region Google Map Example without provider
|
||||
_showPlacePickerInContainer
|
||||
? Container()
|
||||
: ElevatedButton(
|
||||
child: Text("Toggle Google Map w/o Provider"),
|
||||
onPressed: () {
|
||||
initRenderer();
|
||||
setState(() {
|
||||
_showGoogleMapInContainer =
|
||||
!_showGoogleMapInContainer;
|
||||
});
|
||||
},
|
||||
),
|
||||
!_showGoogleMapInContainer
|
||||
? Container()
|
||||
: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.75,
|
||||
height: MediaQuery.of(context).size.height * 0.25,
|
||||
child: GoogleMap(
|
||||
zoomGesturesEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
initialCameraPosition: new CameraPosition(
|
||||
target: GooglePlacePicker.kInitialPosition, zoom: 15),
|
||||
mapType: MapType.normal,
|
||||
myLocationEnabled: true,
|
||||
onMapCreated: (GoogleMapController controller) {},
|
||||
onCameraIdle: () {},
|
||||
onCameraMoveStarted: () {},
|
||||
onCameraMove: (CameraPosition position) {},
|
||||
)),
|
||||
!_showGoogleMapInContainer ? Container() : TextField(),
|
||||
// #endregion
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditPlanRequests extends StatefulWidget {
|
||||
final dynamic order; // Pass order data here
|
||||
|
||||
const EditPlanRequests({super.key, this.order});
|
||||
|
||||
@override
|
||||
State<EditPlanRequests> createState() => _EditPlanRequestsState();
|
||||
}
|
||||
|
||||
class _EditPlanRequestsState extends State<EditPlanRequests> {
|
||||
final TextEditingController tankerPriceController =
|
||||
TextEditingController(text: "2000");
|
||||
final TextEditingController waterTypeController =
|
||||
TextEditingController(text: "Drinking Water");
|
||||
final TextEditingController frequencyController =
|
||||
TextEditingController(text: "4/week");
|
||||
final TextEditingController capacityController =
|
||||
TextEditingController(text: "10000L");
|
||||
final TextEditingController timeController =
|
||||
TextEditingController(text: "10:00 AM - 5:00 PM");
|
||||
final TextEditingController quantityController =
|
||||
TextEditingController(text: "1/day");
|
||||
final TextEditingController advanceController =
|
||||
TextEditingController(text: "40%");
|
||||
final TextEditingController startDateController =
|
||||
TextEditingController(text: "12 July 2025");
|
||||
final TextEditingController endDateController =
|
||||
TextEditingController(text: "21 July 2025");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Update summary in real-time as user types
|
||||
tankerPriceController.addListener(() => setState(() {}));
|
||||
advanceController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int tankerPrice = int.tryParse(tankerPriceController.text) ?? 0;
|
||||
int totalWeeks = 12; // static for now
|
||||
int weeklyPrice = tankerPrice * 4; // assuming 4 tankers/week
|
||||
int totalPrice = weeklyPrice * totalWeeks;
|
||||
double advancePercent =
|
||||
double.tryParse(advanceController.text.replaceAll('%', '')) ?? 0;
|
||||
int advancePayable = (totalPrice * (advancePercent / 100)).round();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
"Edit Order",
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w600),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔹 Club Info Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Chip(
|
||||
label: Text("New",
|
||||
style: TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600)),
|
||||
backgroundColor: Color(0xFFEAF3FF),
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
Text("Club Kohinoor",
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
SizedBox(height: 4),
|
||||
Text("Banjara Hills, Hyderabad",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text("5.5 Km", style: TextStyle(color: Colors.black54)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("ORDER DETAILS",
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||
const SizedBox(height: 12),
|
||||
// 🔹 Two in a row
|
||||
_twoFields(tankerPriceController, "Tanker Price",
|
||||
waterTypeController, "Water Type"),
|
||||
_twoFields(frequencyController, "Frequency", capacityController,
|
||||
"Capacity"),
|
||||
_twoFields(timeController, "Time of Delivery", quantityController,
|
||||
"Quantity"),
|
||||
_twoFields(advanceController, "Advance", startDateController,
|
||||
"Start Date"),
|
||||
_twoFields(endDateController, "End Date", null, null),
|
||||
const SizedBox(height: 20),
|
||||
const Text("UPDATED PAYMENT SUMMARY",
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||
const SizedBox(height: 12),
|
||||
_summaryRow("Tanker Price", "₹ $tankerPrice"),
|
||||
_summaryRow("Tankers per week", "04"),
|
||||
_summaryRow("Weekly Price", "₹ $weeklyPrice"),
|
||||
_summaryRow("Total weeks", "$totalWeeks"),
|
||||
_summaryRow("Total Price", "₹ $totalPrice"),
|
||||
const Divider(),
|
||||
_summaryRow("Advance", advanceController.text),
|
||||
_summaryRow("Advance Payable", "₹ $advancePayable"),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.deepPurple,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
// 🔹 Collect updated values
|
||||
print("Tanker Price: ${tankerPriceController.text}");
|
||||
print("Water Type: ${waterTypeController.text}");
|
||||
print("Frequency: ${frequencyController.text}");
|
||||
// Save logic here
|
||||
},
|
||||
child: const Text("Send ➤"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Two fields side by side
|
||||
Widget _twoFields(TextEditingController? controller1, String? label1,
|
||||
TextEditingController? controller2, String? label2) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _textField(controller1, label1),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (controller2 != null)
|
||||
Expanded(
|
||||
child: _textField(controller2, label2),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Custom text field
|
||||
Widget _textField(TextEditingController? controller, String? label) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Summary row
|
||||
Widget _summaryRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(color: Colors.black54)),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, color: Colors.black)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||