@ -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 localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file("local.properties")
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader ->
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
localProperties.load(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) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = "1"
|
flutterVersionCode = '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty("flutter.versionName")
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
if (flutterVersionName == null) {
|
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 {
|
android {
|
||||||
namespace = "com.arminta.supplier_new"
|
compileSdkVersion 34
|
||||||
compileSdk = flutter.compileSdkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = 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 {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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.
|
// 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.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdkVersion 28
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode = flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName = flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig = signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||