Update project
This commit is contained in:
@@ -18,7 +18,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.nearle.gear"
|
namespace = "com.nearle.gear"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = "28.2.13676358"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.nearle.gear"
|
applicationId = "com.nearle.gear"
|
||||||
@@ -59,6 +59,9 @@ android {
|
|||||||
)}
|
)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
configurations.all {
|
||||||
|
exclude(group = "com.google.firebase", module = "firebase-iid")
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
|||||||
15
android/app/proguard-rules.pro
vendored
15
android/app/proguard-rules.pro
vendored
@@ -53,3 +53,18 @@
|
|||||||
# Keep annotations
|
# Keep annotations
|
||||||
###############################
|
###############################
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
###############################
|
||||||
|
# Firebase InstanceID (legacy, referenced by mlkit-linkfirebase only)
|
||||||
|
###############################
|
||||||
|
-dontwarn com.google.firebase.iid.**
|
||||||
|
|
||||||
|
###############################
|
||||||
|
# ML Kit text recognition — language packs not bundled
|
||||||
|
###############################
|
||||||
|
-dontwarn com.google.mlkit.vision.text.chinese.**
|
||||||
|
-dontwarn com.google.mlkit.vision.text.devanagari.**
|
||||||
|
-dontwarn com.google.mlkit.vision.text.japanese.**
|
||||||
|
-dontwarn com.google.mlkit.vision.text.korean.**
|
||||||
|
-dontwarn com.google.mlkit.linkfirebase.**
|
||||||
|
-dontwarn com.google.mlkit.**
|
||||||
@@ -2,20 +2,17 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FLASHLIGHT" />
|
<uses-permission android:name="android.permission.FLASHLIGHT" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- For Android 13+ -->
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||||
<uses-permission android:name="android.permission.READ_SMS"/>
|
<uses-permission android:name="android.permission.READ_SMS"/>
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/>
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -27,10 +24,13 @@
|
|||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.geo.API_KEY"
|
android:name="com.google.android.geo.API_KEY"
|
||||||
android:value="AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q"/>
|
android:value="AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
android:value="nearle_channel" />
|
android:value="nearle_channel" />
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Google Maps API Key -->
|
<!-- Google Maps API Key -->
|
||||||
|
<meta-data android:name="com.google.android.geo.API_KEY" android:value="AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -74,16 +75,50 @@
|
|||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
|
android:resource="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Deep linking for product sharing -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="nearle" android:host="product" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Google Assistant App Actions -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:scheme="nearle" android:host="order"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
|
|
||||||
|
<receiver android:name=".HomeWidgetProvider" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/home_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.nearle.gear
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import es.antonborri.home_widget.HomeWidgetProvider as BaseHomeWidgetProvider
|
||||||
|
|
||||||
|
class HomeWidgetProvider : BaseHomeWidgetProvider() {
|
||||||
|
|
||||||
|
override fun onUpdate(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetIds: IntArray,
|
||||||
|
widgetData: SharedPreferences
|
||||||
|
) {
|
||||||
|
val path = widgetData.getString("banner_path", null) ?: return
|
||||||
|
val bitmap = BitmapFactory.decodeFile(path) ?: return
|
||||||
|
|
||||||
|
appWidgetIds.forEach { widgetId ->
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.home_widget_layout).apply {
|
||||||
|
setImageViewBitmap(R.id.widget_banner_image, bitmap)
|
||||||
|
}
|
||||||
|
appWidgetManager.updateAppWidget(widgetId, views)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,44 @@
|
|||||||
|
// MainActivity.kt
|
||||||
package com.nearle.gear
|
package com.nearle.gear
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.os.Build
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
private val CHANNEL = "com.nearle.gear/widget"
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
// Register the WebViewFlutterPlugin
|
|
||||||
flutterEngine.plugins.add(WebViewFlutterPlugin())
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"requestPinWidget" -> result.success(requestPinWidget())
|
||||||
|
"isWidgetPinned" -> result.success(isWidgetPinned())
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestPinWidget(): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||||
|
val provider = ComponentName(this, HomeWidgetProvider::class.java)
|
||||||
|
if (appWidgetManager.isRequestPinAppWidgetSupported) {
|
||||||
|
appWidgetManager.requestPinAppWidget(provider, null, null)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWidgetPinned(): Boolean {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||||
|
val provider = ComponentName(this, HomeWidgetProvider::class.java)
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(provider)
|
||||||
|
return ids.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
android/app/src/main/res/layout/home_widget_layout.xml
Normal file
12
android/app/src/main/res/layout/home_widget_layout.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widget_banner_image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
</FrameLayout>
|
||||||
8
android/app/src/main/res/xml/home_widget_info.xml
Normal file
8
android/app/src/main/res/xml/home_widget_info.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="250dp"
|
||||||
|
android:minHeight="100dp"
|
||||||
|
android:updatePeriodMillis="1800000"
|
||||||
|
android:initialLayout="@layout/home_widget_layout"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:widgetCategory="home_screen" />
|
||||||
10
android/app/src/main/res/xml/shortcuts.xml
Normal file
10
android/app/src/main/res/xml/shortcuts.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<capability android:name="actions.intent.ORDER_MENU_ITEM">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetPackage="com.nearle.gear"
|
||||||
|
android:targetClass="com.nearle.gear.MainActivity"
|
||||||
|
android:data="nearle://order?item={menuItem.name}&budget={price.text}"/>
|
||||||
|
</capability>
|
||||||
|
</shortcuts>
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,7 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.7.3" apply false
|
id("com.android.application") version "8.9.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB |
1
assets/lotties/ai.json
Normal file
1
assets/lotties/ai.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/lotties/ai_loader.json
Normal file
1
assets/lotties/ai_loader.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/lotties/voice.json
Normal file
1
assets/lotties/voice.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '14.0'
|
platform :ios, '15.5'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
179
ios/Podfile.lock
179
ios/Podfile.lock
@@ -1,29 +1,118 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_contacts (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Google-Maps-iOS-Utils (5.0.0):
|
- Google-Maps-iOS-Utils (6.1.0):
|
||||||
- GoogleMaps (~> 8.0)
|
- GoogleMaps (~> 9.0)
|
||||||
- google_maps_flutter_ios (0.0.1):
|
- google_maps_flutter_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Google-Maps-iOS-Utils (< 7.0, >= 5.0)
|
- Google-Maps-iOS-Utils (< 7.0, >= 5.0)
|
||||||
- GoogleMaps (< 10.0, >= 8.4)
|
- GoogleMaps (< 10.0, >= 8.4)
|
||||||
- GoogleMaps (8.4.0):
|
- google_mlkit_commons (0.8.1):
|
||||||
- GoogleMaps/Maps (= 8.4.0)
|
- Flutter
|
||||||
- GoogleMaps/Base (8.4.0)
|
- MLKitVision
|
||||||
- GoogleMaps/Maps (8.4.0):
|
- google_mlkit_object_detection (0.13.1):
|
||||||
- GoogleMaps/Base
|
- Flutter
|
||||||
|
- google_mlkit_commons
|
||||||
|
- GoogleMLKit/ObjectDetection (~> 6.0.0)
|
||||||
|
- GoogleMLKit/ObjectDetectionCustom (~> 6.0.0)
|
||||||
|
- google_mlkit_text_recognition (0.13.1):
|
||||||
|
- Flutter
|
||||||
|
- google_mlkit_commons
|
||||||
|
- GoogleMLKit/TextRecognition (~> 6.0.0)
|
||||||
|
- GoogleDataTransport (9.4.1):
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- GoogleMaps (9.4.0):
|
||||||
|
- GoogleMaps/Maps (= 9.4.0)
|
||||||
|
- GoogleMaps/Maps (9.4.0)
|
||||||
|
- GoogleMLKit/MLKitCore (6.0.0):
|
||||||
|
- MLKitCommon (~> 11.0.0)
|
||||||
|
- GoogleMLKit/ObjectDetection (6.0.0):
|
||||||
|
- GoogleMLKit/MLKitCore
|
||||||
|
- MLKitObjectDetection (~> 5.0.0)
|
||||||
|
- GoogleMLKit/ObjectDetectionCustom (6.0.0):
|
||||||
|
- GoogleMLKit/MLKitCore
|
||||||
|
- MLKitObjectDetectionCustom (~> 5.0.0)
|
||||||
|
- GoogleMLKit/TextRecognition (6.0.0):
|
||||||
|
- GoogleMLKit/MLKitCore
|
||||||
|
- MLKitTextRecognition (~> 4.0.0)
|
||||||
|
- GoogleToolboxForMac/Defines (4.2.1)
|
||||||
|
- GoogleToolboxForMac/Logger (4.2.1):
|
||||||
|
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||||
|
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||||
|
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||||
|
- GoogleUtilities/Environment (7.13.3):
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- GoogleUtilities/Logger (7.13.3):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Privacy (7.13.3)
|
||||||
|
- GoogleUtilities/UserDefaults (7.13.3):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilitiesComponents (1.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GTMSessionFetcher/Core (3.5.0)
|
||||||
|
- home_widget (0.0.1):
|
||||||
|
- Flutter
|
||||||
- location (0.0.1):
|
- location (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- MLImage (1.0.0-beta5)
|
||||||
|
- MLKitCommon (11.0.0):
|
||||||
|
- GoogleDataTransport (< 10.0, >= 9.4.1)
|
||||||
|
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||||
|
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||||
|
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
|
||||||
|
- GoogleUtilitiesComponents (~> 1.0)
|
||||||
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||||
|
- MLKitImageLabelingCommon (7.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- MLKitObjectDetection (5.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitObjectDetectionCommon (~> 7.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- MLKitVisionKit (~> 8.0)
|
||||||
|
- MLKitObjectDetectionCommon (7.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- MLKitObjectDetectionCustom (5.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitObjectDetectionCommon (~> 7.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- MLKitVisionKit (~> 8.0)
|
||||||
|
- MLKitTextRecognition (4.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitTextRecognitionCommon (= 3.0.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- MLKitTextRecognitionCommon (3.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- MLKitVision (7.0.0):
|
||||||
|
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||||
|
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||||
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||||
|
- MLImage (= 1.0.0-beta5)
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitVisionKit (8.0.0):
|
||||||
|
- MLKitCommon (~> 11.0)
|
||||||
|
- MLKitImageLabelingCommon (~> 7.0)
|
||||||
|
- MLKitObjectDetectionCommon (~> 7.0)
|
||||||
|
- MLKitVision (~> 7.0)
|
||||||
|
- nanopb (2.30910.0):
|
||||||
|
- nanopb/decode (= 2.30910.0)
|
||||||
|
- nanopb/encode (= 2.30910.0)
|
||||||
|
- nanopb/decode (2.30910.0)
|
||||||
|
- nanopb/encode (2.30910.0)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- razorpay-core-pod (1.0.6)
|
- PromisesObjC (2.4.1)
|
||||||
- razorpay-pod (1.5.3):
|
- razorpay-pod (1.5.4)
|
||||||
- razorpay-core-pod (= 1.0.6)
|
|
||||||
- razorpay_flutter (1.1.10):
|
- razorpay_flutter (1.1.10):
|
||||||
- Flutter
|
- Flutter
|
||||||
- razorpay-pod
|
- razorpay-pod
|
||||||
@@ -32,9 +121,12 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`)
|
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
|
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
|
||||||
|
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
|
||||||
|
- google_mlkit_object_detection (from `.symlinks/plugins/google_mlkit_object_detection/ios`)
|
||||||
|
- google_mlkit_text_recognition (from `.symlinks/plugins/google_mlkit_text_recognition/ios`)
|
||||||
|
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||||
- location (from `.symlinks/plugins/location/ios`)
|
- location (from `.symlinks/plugins/location/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
@@ -44,19 +136,42 @@ DEPENDENCIES:
|
|||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- Google-Maps-iOS-Utils
|
- Google-Maps-iOS-Utils
|
||||||
|
- GoogleDataTransport
|
||||||
- GoogleMaps
|
- GoogleMaps
|
||||||
- razorpay-core-pod
|
- GoogleMLKit
|
||||||
|
- GoogleToolboxForMac
|
||||||
|
- GoogleUtilities
|
||||||
|
- GoogleUtilitiesComponents
|
||||||
|
- GTMSessionFetcher
|
||||||
|
- MLImage
|
||||||
|
- MLKitCommon
|
||||||
|
- MLKitImageLabelingCommon
|
||||||
|
- MLKitObjectDetection
|
||||||
|
- MLKitObjectDetectionCommon
|
||||||
|
- MLKitObjectDetectionCustom
|
||||||
|
- MLKitTextRecognition
|
||||||
|
- MLKitTextRecognitionCommon
|
||||||
|
- MLKitVision
|
||||||
|
- MLKitVisionKit
|
||||||
|
- nanopb
|
||||||
|
- PromisesObjC
|
||||||
- razorpay-pod
|
- razorpay-pod
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_contacts:
|
|
||||||
:path: ".symlinks/plugins/flutter_contacts/ios"
|
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
google_maps_flutter_ios:
|
google_maps_flutter_ios:
|
||||||
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
|
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
|
||||||
|
google_mlkit_commons:
|
||||||
|
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
||||||
|
google_mlkit_object_detection:
|
||||||
|
:path: ".symlinks/plugins/google_mlkit_object_detection/ios"
|
||||||
|
google_mlkit_text_recognition:
|
||||||
|
:path: ".symlinks/plugins/google_mlkit_text_recognition/ios"
|
||||||
|
home_widget:
|
||||||
|
:path: ".symlinks/plugins/home_widget/ios"
|
||||||
location:
|
location:
|
||||||
:path: ".symlinks/plugins/location/ios"
|
:path: ".symlinks/plugins/location/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
@@ -70,19 +185,39 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_contacts: 5383945387e7ca37cf963d4be57c21f2fc15ca9f
|
|
||||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||||
Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321
|
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
|
||||||
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264
|
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264
|
||||||
GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d
|
google_mlkit_commons: 414fc36ac51758c806381de33a1d148d2ba71ce4
|
||||||
|
google_mlkit_object_detection: 888a9864123f5ef0e0a8c8d3f1d9f6c14393840d
|
||||||
|
google_mlkit_text_recognition: 6dd0122107954e64667d826da71de686152109fe
|
||||||
|
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||||
|
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
|
||||||
|
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
|
||||||
|
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||||
|
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||||
|
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
||||||
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
|
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||||
location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8
|
location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8
|
||||||
|
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
||||||
|
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
|
||||||
|
MLKitImageLabelingCommon: fa802f14c3a321121409f6cbe1df89ba9cf1a00c
|
||||||
|
MLKitObjectDetection: 9b40359730baceef23322c6d41070da7639ff100
|
||||||
|
MLKitObjectDetectionCommon: f3ca8581a0741be6ce609920367316571a280efa
|
||||||
|
MLKitObjectDetectionCustom: a412a63a743724ae124b07d6c3cc817283da3453
|
||||||
|
MLKitTextRecognition: c83c18ad25496f2077f6ec93c5940487ff2eb343
|
||||||
|
MLKitTextRecognitionCommon: c0b3a63d529296a19bce1f8bc8a513644ed4d1f6
|
||||||
|
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
|
||||||
|
MLKitVisionKit: 6d2ea9741b262ec0fed2a5a3de521cfa671c6e83
|
||||||
|
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||||
package_info_plus: 566e1b7a2f3900e4b0020914ad3fc051dcc95596
|
package_info_plus: 566e1b7a2f3900e4b0020914ad3fc051dcc95596
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
razorpay-core-pod: 6da5fb4ed280279d665507fdb50e213d793a5fe2
|
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
|
||||||
razorpay-pod: 4385cf844aa29389313b741e20c72fe668970d49
|
razorpay-pod: 3ba80419f3315352d6ffa336a194ad95ab249700
|
||||||
razorpay_flutter: 0e98e4fcaae27ad50e011d85f66d85e0a008754a
|
razorpay_flutter: 0e98e4fcaae27ad50e011d85f66d85e0a008754a
|
||||||
sms_autofill: b36b2147535657fea83d7f3898d7831de70bd8e4
|
sms_autofill: b36b2147535657fea83d7f3898d7831de70bd8e4
|
||||||
|
|
||||||
PODFILE CHECKSUM: a901b0534ebff1a81cfb53fbf723b1bca433de9f
|
PODFILE CHECKSUM: 5cbaa2e976150d6ca52c31ab6e60efa2670a60bc
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
496924C4B012DE93FE3F4ECA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 415548DB485576CB7FE174F9 /* Pods_Runner.framework */; };
|
|
||||||
54D9B915190C58FAD7127809 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F81FF6F2D63FF725573805D7 /* Pods_RunnerTests.framework */; };
|
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
|
7986347451EEDDA598144EBD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42AF458A19FA87D6B6222669 /* Pods_RunnerTests.framework */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
BF8C3AB8B374BC2E78DCF9F9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B6582B226A1F9C6F60BA8FE /* Pods_Runner.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -48,14 +48,15 @@
|
|||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
415548DB485576CB7FE174F9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
42AF458A19FA87D6B6222669 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5DA0CC029FD23DB3E018EF32 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
48D2495DDCC4C0EA5C0C70BF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
6DEB7185559E0DA885F8BBB2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
73038318DDCE8E5C422FFDAC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
7A48B292DCD250363BDB1844 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
7B6582B226A1F9C6F60BA8FE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
81CC33AFB41B5982FB30E75D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -63,10 +64,9 @@
|
|||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
C30F8E9BE0DAA4B0BBDA9B05 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
A60CCE72F8F1070896C58BC3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
D34FC3C2D59EE0677BA16625 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
E941989A32F6914B4203541B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
DFFC5DB4F11E559DDA8A923E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
FB293B10077F8F9C79ED3261 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
F81FF6F2D63FF725573805D7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
496924C4B012DE93FE3F4ECA /* Pods_Runner.framework in Frameworks */,
|
BF8C3AB8B374BC2E78DCF9F9 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
54D9B915190C58FAD7127809 /* Pods_RunnerTests.framework in Frameworks */,
|
7986347451EEDDA598144EBD /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
D6A52DD0F305C2721292572B /* Pods */,
|
D6A52DD0F305C2721292572B /* Pods */,
|
||||||
A8EB2AA264F77BD42DF91F7E /* Frameworks */,
|
B515A6DBF5715200D9D48CDA /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -146,11 +146,11 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A8EB2AA264F77BD42DF91F7E /* Frameworks */ = {
|
B515A6DBF5715200D9D48CDA /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
415548DB485576CB7FE174F9 /* Pods_Runner.framework */,
|
7B6582B226A1F9C6F60BA8FE /* Pods_Runner.framework */,
|
||||||
F81FF6F2D63FF725573805D7 /* Pods_RunnerTests.framework */,
|
42AF458A19FA87D6B6222669 /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -158,12 +158,12 @@
|
|||||||
D6A52DD0F305C2721292572B /* Pods */ = {
|
D6A52DD0F305C2721292572B /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6DEB7185559E0DA885F8BBB2 /* Pods-Runner.debug.xcconfig */,
|
81CC33AFB41B5982FB30E75D /* Pods-Runner.debug.xcconfig */,
|
||||||
5DA0CC029FD23DB3E018EF32 /* Pods-Runner.release.xcconfig */,
|
E941989A32F6914B4203541B /* Pods-Runner.release.xcconfig */,
|
||||||
C30F8E9BE0DAA4B0BBDA9B05 /* Pods-Runner.profile.xcconfig */,
|
73038318DDCE8E5C422FFDAC /* Pods-Runner.profile.xcconfig */,
|
||||||
D34FC3C2D59EE0677BA16625 /* Pods-RunnerTests.debug.xcconfig */,
|
FB293B10077F8F9C79ED3261 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
DFFC5DB4F11E559DDA8A923E /* Pods-RunnerTests.release.xcconfig */,
|
48D2495DDCC4C0EA5C0C70BF /* Pods-RunnerTests.release.xcconfig */,
|
||||||
7A48B292DCD250363BDB1844 /* Pods-RunnerTests.profile.xcconfig */,
|
A60CCE72F8F1070896C58BC3 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
1D8A7B8D320B90CD1E12A7EF /* [CP] Check Pods Manifest.lock */,
|
47F6B4685984264CA6501959 /* [CP] Check Pods Manifest.lock */,
|
||||||
331C807D294A63A400263BE5 /* Sources */,
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
331C807F294A63A400263BE5 /* Resources */,
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
D2D8874BAFC912C92F358E77 /* Frameworks */,
|
D2D8874BAFC912C92F358E77 /* Frameworks */,
|
||||||
@@ -194,15 +194,15 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
EA24EB15C10E42710DBCFB14 /* [CP] Check Pods Manifest.lock */,
|
C5E75C1082B7CA6F769A3C87 /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
D51672018CD7C6B1266BE9E3 /* [CP] Embed Pods Frameworks */,
|
83DD7C5BA49A69DFF78E0954 /* [CP] Embed Pods Frameworks */,
|
||||||
A7A93A12585D43A616D73706 /* [CP] Copy Pods Resources */,
|
80FE172A180B5CF341443AC5 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -280,7 +280,23 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
1D8A7B8D320B90CD1E12A7EF /* [CP] Check Pods Manifest.lock */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
47F6B4685984264CA6501959 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -302,38 +318,7 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
80FE172A180B5CF341443AC5 /* [CP] Copy Pods Resources */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
|
||||||
);
|
|
||||||
name = "Thin Binary";
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
|
||||||
};
|
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "Run Script";
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
|
||||||
};
|
|
||||||
A7A93A12585D43A616D73706 /* [CP] Copy Pods Resources */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -350,7 +335,7 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
D51672018CD7C6B1266BE9E3 /* [CP] Embed Pods Frameworks */ = {
|
83DD7C5BA49A69DFF78E0954 /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -367,7 +352,22 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
EA24EB15C10E42710DBCFB14 /* [CP] Check Pods Manifest.lock */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
C5E75C1082B7CA6F769A3C87 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
};
|
};
|
||||||
331C8088294A63A400263BE5 /* Debug */ = {
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = D34FC3C2D59EE0677BA16625 /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = FB293B10077F8F9C79ED3261 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -533,7 +533,7 @@
|
|||||||
};
|
};
|
||||||
331C8089294A63A400263BE5 /* Release */ = {
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = DFFC5DB4F11E559DDA8A923E /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = 48D2495DDCC4C0EA5C0C70BF /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
};
|
};
|
||||||
331C808A294A63A400263BE5 /* Profile */ = {
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7A48B292DCD250363BDB1844 /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = A60CCE72F8F1070896C58BC3 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|||||||
@@ -18,6 +18,15 @@
|
|||||||
"version" : "11.2.0"
|
"version" : "11.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "cwlcatchexception",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "07b2ba21d361c223e25e3c1e924288742923f08c",
|
||||||
|
"version" : "2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "firebase-ios-sdk",
|
"identity" : "firebase-ios-sdk",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -18,6 +18,15 @@
|
|||||||
"version" : "11.2.0"
|
"version" : "11.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "cwlcatchexception",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "07b2ba21d361c223e25e3c1e924288742923f08c",
|
||||||
|
"version" : "2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "firebase-ios-sdk",
|
"identity" : "firebase-ios-sdk",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -34,29 +34,10 @@
|
|||||||
<string>Required for voice-based payment authentication</string>
|
<string>Required for voice-based payment authentication</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Required to upload payment screenshots</string>
|
<string>Required to upload payment screenshots</string>
|
||||||
<key>UIApplicationSceneManifest</key>
|
|
||||||
<dict>
|
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
|
||||||
<false/>
|
|
||||||
<key>UISceneConfigurations</key>
|
|
||||||
<dict>
|
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>UISceneClassName</key>
|
|
||||||
<string>UIWindowScene</string>
|
|
||||||
<key>UISceneConfigurationName</key>
|
|
||||||
<string>flutter</string>
|
|
||||||
<key>UISceneDelegateClassName</key>
|
|
||||||
<string>FlutterSceneDelegate</string>
|
|
||||||
<key>UISceneStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -14,16 +15,20 @@ import '../../constants/error_constants.dart';
|
|||||||
import '../../data/authentication/auth_request.dart';
|
import '../../data/authentication/auth_request.dart';
|
||||||
import '../../data/authentication/auth_response.dart';
|
import '../../data/authentication/auth_response.dart';
|
||||||
import '../../domain/repository/authentication/auth_repository.dart';
|
import '../../domain/repository/authentication/auth_repository.dart';
|
||||||
|
import '../../service/firebase_analytics/analytics_service.dart';
|
||||||
import '../../view/authentication/costomer_create_view.dart';
|
import '../../view/authentication/costomer_create_view.dart';
|
||||||
import '../../view/authentication/verification_view.dart';
|
import '../../view/authentication/verification_view.dart';
|
||||||
import '../../view/dashboard_view/dashboard_view.dart';
|
import '../../view/dashboard_view/dashboard_view.dart';
|
||||||
import '../../view/home_view.dart';
|
import '../../view/home_view.dart';
|
||||||
import '../tenant_controller /tenant_list.dart';
|
import '../tenant_list/tenant_controller.dart';
|
||||||
|
|
||||||
class AuthController extends GetxController {
|
class AuthController extends GetxController {
|
||||||
|
|
||||||
|
final FirebaseAnalytics analytics = FirebaseAnalytics.instance;
|
||||||
LoginRepository loginRepository = LoginRepository();
|
LoginRepository loginRepository = LoginRepository();
|
||||||
final TenantController tenantControllers = Get.put(TenantController());
|
final TenantController tenantControllers = Get.put(TenantController());
|
||||||
|
|
||||||
|
|
||||||
var isLoading = false.obs;
|
var isLoading = false.obs;
|
||||||
|
|
||||||
int? activeStatus;
|
int? activeStatus;
|
||||||
@@ -129,6 +134,16 @@ class AuthController extends GetxController {
|
|||||||
|
|
||||||
ErrorConstants.apiError.value = false;
|
ErrorConstants.apiError.value = false;
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'login_success',
|
||||||
|
parameters: {
|
||||||
|
'customer_id': customerId.toString(),
|
||||||
|
'name': result?.details?.firstname ?? 'unknown',
|
||||||
|
'email': result?.details?.email ?? 'unknown',
|
||||||
|
'city': result?.details?.suburb ?? 'unknown',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
isNewUser=false;
|
isNewUser=false;
|
||||||
|
|
||||||
validateDevice(deviceId ?? '');
|
validateDevice(deviceId ?? '');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import '../../modules/authentication/auth.dart';
|
|||||||
import '../../modules/product/product.dart';
|
import '../../modules/product/product.dart';
|
||||||
import '../../modules/tenant/get_tenant.dart' hide Customer;
|
import '../../modules/tenant/get_tenant.dart' hide Customer;
|
||||||
import '../../service/dio.dart';
|
import '../../service/dio.dart';
|
||||||
import '../tenant_controller /tenant_list.dart'; // New Product modules
|
import '../tenant_list/tenant_controller.dart';
|
||||||
|
|
||||||
class CartController extends GetxController {
|
class CartController extends GetxController {
|
||||||
var cartItems = <CartItem>[].obs;
|
var cartItems = <CartItem>[].obs;
|
||||||
@@ -606,6 +606,7 @@ class CartController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Clear cart
|
/// Clear cart
|
||||||
void clearCart() => cartItems.clear();
|
void clearCart() => cartItems.clear();
|
||||||
|
|
||||||
@@ -617,6 +618,8 @@ class CartController extends GetxController {
|
|||||||
cartItems.fold(0, (sum, item) => sum + ((item.product.productcost ?? 0) * item.quantity));
|
cartItems.fold(0, (sum, item) => sum + ((item.product.productcost ?? 0) * item.quantity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Cart item class
|
/// Cart item class
|
||||||
class CartItem {
|
class CartItem {
|
||||||
final Product product;
|
final Product product;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import '../../modules/tenant/category.dart';
|
|||||||
import '../../modules/tenant/get_tenant.dart';
|
import '../../modules/tenant/get_tenant.dart';
|
||||||
import '../../view/authentication/costomer_create_view.dart';
|
import '../../view/authentication/costomer_create_view.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
import '../tenant_controller /tenant_list.dart';
|
import '../tenant_list/tenant_controller.dart';
|
||||||
|
|
||||||
class DashboardController extends GetxController {
|
class DashboardController extends GetxController {
|
||||||
// Loading state
|
// Loading state
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../modules/orders/create_order.dart';
|
|||||||
class OrderController extends GetxController {
|
class OrderController extends GetxController {
|
||||||
final CreateOrderProvider provider = CreateOrderProvider();
|
final CreateOrderProvider provider = CreateOrderProvider();
|
||||||
var isLoading = false.obs;
|
var isLoading = false.obs;
|
||||||
|
String? lastError; // 👈 add this
|
||||||
|
|
||||||
Future<CreateOrderResponse?> createOrder(CreateOrderRequest request) async {
|
Future<CreateOrderResponse?> createOrder(CreateOrderRequest request) async {
|
||||||
try {
|
try {
|
||||||
@@ -17,20 +18,19 @@ class OrderController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
if (response.status == 'accepted') {
|
if (response.status == 'accepted') {
|
||||||
|
|
||||||
print(response.status);
|
print(response.status);
|
||||||
|
|
||||||
print("✅ Order Success");
|
print("✅ Order Success");
|
||||||
} else {
|
} else {
|
||||||
print("❌ Order Failed");
|
print("❌ Order Failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response; // ✅ VERY IMPORTANT
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
print("🔥 ERROR: $e");
|
print("🔥 ERROR: $e"); // already printing e here
|
||||||
|
print("🔥 ERROR TYPE: ${e.runtimeType}"); // 👈 add this to see the exact class
|
||||||
|
|
||||||
return null; // ✅ return null on error
|
return null;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../service/ai_service/api_service.dart';
|
||||||
|
|
||||||
|
class AiSearchController extends GetxController {
|
||||||
|
final AiSearchService service = AiSearchService();
|
||||||
|
|
||||||
|
RxBool loading = false.obs;
|
||||||
|
RxMap<String, dynamic> result = <String, dynamic>{}.obs;
|
||||||
|
|
||||||
|
Future<void> search(String query) async {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
final data = await service.search(query);
|
||||||
|
|
||||||
|
result.value = data;
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error",
|
||||||
|
e.toString(),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
485
lib/controllers/search_controller/product_match.dart
Normal file
485
lib/controllers/search_controller/product_match.dart
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import '../../modules/search/grocery_item.dart';
|
||||||
|
|
||||||
|
// ─── Models ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class StoreProduct {
|
||||||
|
final int productId;
|
||||||
|
final String productName;
|
||||||
|
final String productImage;
|
||||||
|
final double productCost;
|
||||||
|
final double discountValue;
|
||||||
|
final String productUnit;
|
||||||
|
final String unitValue;
|
||||||
|
final String productStatus;
|
||||||
|
final int subcategoryId;
|
||||||
|
final String subcategoryName;
|
||||||
|
|
||||||
|
const StoreProduct({
|
||||||
|
required this.productId,
|
||||||
|
required this.productName,
|
||||||
|
required this.productImage,
|
||||||
|
required this.productCost,
|
||||||
|
required this.discountValue,
|
||||||
|
required this.productUnit,
|
||||||
|
required this.unitValue,
|
||||||
|
required this.productStatus,
|
||||||
|
required this.subcategoryId,
|
||||||
|
required this.subcategoryName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory StoreProduct.fromJson(Map<String, dynamic> json, String subcategoryName) {
|
||||||
|
return StoreProduct(
|
||||||
|
productId: json['productid'] ?? 0,
|
||||||
|
productName: json['productname'] ?? '',
|
||||||
|
productImage: json['productimage'] ?? '',
|
||||||
|
productCost: (json['productcost'] ?? 0).toDouble(),
|
||||||
|
discountValue: (json['discountvalue'] ?? 0).toDouble(),
|
||||||
|
productUnit: json['productunit'] ?? '',
|
||||||
|
unitValue: json['unitvalue'] ?? '',
|
||||||
|
productStatus: json['productstatus'] ?? '',
|
||||||
|
subcategoryId: json['subcategoryid'] ?? 0,
|
||||||
|
subcategoryName: subcategoryName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isAvailable => productStatus == 'available';
|
||||||
|
|
||||||
|
double get finalPrice {
|
||||||
|
if (discountValue <= 0) return productCost;
|
||||||
|
return productCost - (productCost * discountValue / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoreInfo {
|
||||||
|
final int tenantId;
|
||||||
|
final String tenantName;
|
||||||
|
final String address;
|
||||||
|
final String suburb;
|
||||||
|
final String city;
|
||||||
|
final String latitude;
|
||||||
|
final String longitude;
|
||||||
|
final int locationId;
|
||||||
|
final String locationName;
|
||||||
|
final int categoryId;
|
||||||
|
final int ordersCount;
|
||||||
|
|
||||||
|
const StoreInfo({
|
||||||
|
required this.tenantId,
|
||||||
|
required this.tenantName,
|
||||||
|
required this.address,
|
||||||
|
required this.suburb,
|
||||||
|
required this.city,
|
||||||
|
required this.latitude,
|
||||||
|
required this.longitude,
|
||||||
|
required this.locationId,
|
||||||
|
required this.locationName,
|
||||||
|
required this.categoryId,
|
||||||
|
required this.ordersCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory StoreInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
return StoreInfo(
|
||||||
|
tenantId: json['tenantid'] ?? 0,
|
||||||
|
tenantName: json['tenantname'] ?? '',
|
||||||
|
address: json['address'] ?? '',
|
||||||
|
suburb: json['suburb'] ?? '',
|
||||||
|
city: json['city'] ?? '',
|
||||||
|
latitude: json['latitude'] ?? '',
|
||||||
|
longitude: json['longitude'] ?? '',
|
||||||
|
locationId: json['locationid'] ?? 0,
|
||||||
|
locationName: json['locationname'] ?? '',
|
||||||
|
categoryId: json['categoryid'] ?? 0,
|
||||||
|
ordersCount: json['orderscount'] ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single AI item matched to one or more store products across stores.
|
||||||
|
class MatchedItem {
|
||||||
|
final GroceryItem aiItem;
|
||||||
|
|
||||||
|
/// storeName → list of matched products in that store
|
||||||
|
final Map<String, List<StoreProduct>> storeMatches;
|
||||||
|
|
||||||
|
const MatchedItem({
|
||||||
|
required this.aiItem,
|
||||||
|
required this.storeMatches,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get hasMatches => storeMatches.isNotEmpty;
|
||||||
|
|
||||||
|
int get totalMatchCount =>
|
||||||
|
storeMatches.values.fold(0, (sum, list) => sum + list.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Controller ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class StoreMatchController extends ChangeNotifier {
|
||||||
|
// ── Config ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static const int _customerId = 6060;
|
||||||
|
static const double _latitude = 11.0050728;
|
||||||
|
static const double _longitude = 76.9508513;
|
||||||
|
|
||||||
|
static const String _baseUrl = 'https://fiesta.nearle.app/live/api/v1/mob';
|
||||||
|
static const String _tenantsEndpoint = '$_baseUrl/tenants/getcustomertenants';
|
||||||
|
static const String _productsEndpoint = '$_baseUrl/products/getproductsbysubcategory';
|
||||||
|
|
||||||
|
static const String _groqApiKey =
|
||||||
|
'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';
|
||||||
|
static const String _groqUrl =
|
||||||
|
'https://api.groq.com/openai/v1/chat/completions';
|
||||||
|
|
||||||
|
// ── State ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool loading = false;
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
List<StoreInfo> stores = [];
|
||||||
|
final Map<String, List<StoreProduct>> _storeProducts = {};
|
||||||
|
List<MatchedItem> matchedResults = [];
|
||||||
|
|
||||||
|
// ── Public entry point ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> matchAiItems(List<GroceryItem> aiItems) async {
|
||||||
|
if (aiItems.isEmpty) return;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
matchedResults = [];
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1 — fetch all nearby stores
|
||||||
|
await _fetchStores();
|
||||||
|
|
||||||
|
if (stores.isEmpty) {
|
||||||
|
error = 'No stores found nearby.';
|
||||||
|
loading = false;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2 — fetch products for every store concurrently
|
||||||
|
await _fetchAllStoreProducts();
|
||||||
|
|
||||||
|
// Step 3 — AI matching
|
||||||
|
await _performAiMatching(aiItems);
|
||||||
|
|
||||||
|
// Step 4 — print results
|
||||||
|
_printMatchResults();
|
||||||
|
|
||||||
|
} catch (e, st) {
|
||||||
|
error = 'Something went wrong: $e';
|
||||||
|
debugPrint('[StoreMatch] Error: $e\n$st');
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 1: Fetch stores ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _fetchStores() async {
|
||||||
|
final uri = Uri.parse(_tenantsEndpoint).replace(queryParameters: {
|
||||||
|
'customerid': '$_customerId',
|
||||||
|
'tenant': '0',
|
||||||
|
'latitude': '$_latitude',
|
||||||
|
'longitude': '$_longitude',
|
||||||
|
'categoryid': '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
debugPrint('[StoreMatch] Fetching stores → $uri');
|
||||||
|
|
||||||
|
final res = await http.get(uri);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception('Stores API error ${res.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final json = jsonDecode(res.body) as Map<String, dynamic>;
|
||||||
|
if (json['status'] != true) {
|
||||||
|
throw Exception('Stores API returned status=false: ${json['message']}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final details = (json['details'] as List?) ?? [];
|
||||||
|
|
||||||
|
final seen = <String>{};
|
||||||
|
stores = details
|
||||||
|
.map((d) => StoreInfo.fromJson(d as Map<String, dynamic>))
|
||||||
|
.where((s) {
|
||||||
|
final key = '${s.tenantId}_${s.locationId}';
|
||||||
|
return seen.add(key);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
debugPrint('[StoreMatch] Fetched ${stores.length} store-location(s).');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: Fetch products for every store ────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _fetchAllStoreProducts() async {
|
||||||
|
await Future.wait(stores.map(_fetchProductsForStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchProductsForStore(StoreInfo store) async {
|
||||||
|
final storeKey = '${store.tenantId}_${store.locationId}';
|
||||||
|
|
||||||
|
final uri = Uri.parse(_productsEndpoint).replace(queryParameters: {
|
||||||
|
'categoryid': '${store.categoryId == 0 ? 2 : store.categoryId}',
|
||||||
|
'tenantid': '${store.tenantId}',
|
||||||
|
'locationid': '${store.locationId}',
|
||||||
|
});
|
||||||
|
|
||||||
|
debugPrint('[StoreMatch] Fetching products for ${store.tenantName} → $uri');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await http.get(uri);
|
||||||
|
if (res.statusCode != 200) return;
|
||||||
|
|
||||||
|
final json = jsonDecode(res.body) as Map<String, dynamic>;
|
||||||
|
if (json['status'] != true || json['data'] == null) return;
|
||||||
|
|
||||||
|
final details = (json['data']['details'] as List?) ?? [];
|
||||||
|
|
||||||
|
final products = <StoreProduct>[];
|
||||||
|
for (final cat in details) {
|
||||||
|
final subcatName = (cat['subcategoryname'] as String?) ?? '';
|
||||||
|
final productList = (cat['products'] as List?) ?? [];
|
||||||
|
for (final p in productList) {
|
||||||
|
products.add(
|
||||||
|
StoreProduct.fromJson(p as Map<String, dynamic>, subcatName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_storeProducts[storeKey] = products;
|
||||||
|
debugPrint(
|
||||||
|
'[StoreMatch] ${store.tenantName} (${store.locationName}): '
|
||||||
|
'${products.length} products loaded.',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[StoreMatch] Failed to fetch products for $storeKey: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: AI matching ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> _performAiMatching(List<GroceryItem> aiItems) async {
|
||||||
|
// Build the AI item list string
|
||||||
|
final aiItemsList = aiItems
|
||||||
|
.map((item) => '- ${item.name} (${item.quantity})')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Build the store products list string
|
||||||
|
// Format: "storeName — locationName|productName"
|
||||||
|
final productLines = <String>[];
|
||||||
|
for (final store in stores) {
|
||||||
|
final storeKey = '${store.tenantId}_${store.locationId}';
|
||||||
|
final products = _storeProducts[storeKey] ?? [];
|
||||||
|
final storeLabel = '${store.tenantName} — ${store.locationName}';
|
||||||
|
for (final p in products) {
|
||||||
|
productLines.add('$storeLabel|${p.productName}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productLines.isEmpty) {
|
||||||
|
debugPrint('[StoreMatch] No store products to match against.');
|
||||||
|
matchedResults = aiItems
|
||||||
|
.map((item) => MatchedItem(aiItem: item, storeMatches: {}))
|
||||||
|
.toList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final productListString = productLines.join('\n');
|
||||||
|
|
||||||
|
debugPrint('[StoreMatch] Sending ${aiItems.length} AI items and '
|
||||||
|
'${productLines.length} store products to Groq for matching...');
|
||||||
|
|
||||||
|
final prompt = '''
|
||||||
|
You are a strict grocery product matching engine.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Match each AI item ONLY to products whose name is genuinely the same item.
|
||||||
|
- "Chicken Biryani" must ONLY match products containing "biryani" or "chicken biryani".
|
||||||
|
- "Curd" must NEVER match "Biryani". "Eggs" must NEVER match "Rice".
|
||||||
|
- Be flexible with brand names: "Basmati Rice" matches "India Gate Basmati Rice 1kg".
|
||||||
|
- If you are not confident, return an empty matches array.
|
||||||
|
- The "matches" array must contain EXACT product names copied from the STORE PRODUCTS list below. Do NOT paraphrase or modify them.
|
||||||
|
- Return ONLY a raw JSON array. No markdown, no explanation, nothing else.
|
||||||
|
|
||||||
|
Each element must have:
|
||||||
|
- "aiItem": the exact AI item name from the list below
|
||||||
|
- "matches": array of exact product name strings from the store products list
|
||||||
|
|
||||||
|
═══════════════════════
|
||||||
|
AI ITEMS (user is looking for):
|
||||||
|
$aiItemsList
|
||||||
|
|
||||||
|
═══════════════════════
|
||||||
|
STORE PRODUCTS (format: storeName|productName):
|
||||||
|
$productListString
|
||||||
|
═══════════════════════
|
||||||
|
|
||||||
|
Return format example:
|
||||||
|
[
|
||||||
|
{"aiItem": "Chicken Biryani", "matches": ["Chicken Biryani (Full)", "Biryani Chicken"]},
|
||||||
|
{"aiItem": "Eggs", "matches": ["Farm Fresh Eggs 6pcs"]},
|
||||||
|
{"aiItem": "Pizza", "matches": []}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse(_groqUrl),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $_groqApiKey',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'model': 'llama-3.3-70b-versatile',
|
||||||
|
'max_tokens': 1000,
|
||||||
|
'messages': [
|
||||||
|
{'role': 'user', 'content': prompt},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception('Groq matching API error ${res.statusCode}: ${res.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||||
|
final content = data['choices'][0]['message']['content'] as String;
|
||||||
|
|
||||||
|
// Extract JSON array from response
|
||||||
|
final start = content.indexOf('[');
|
||||||
|
final end = content.lastIndexOf(']') + 1;
|
||||||
|
if (start == -1 || end == 0) {
|
||||||
|
throw Exception('No JSON array in Groq matching response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final List parsed = jsonDecode(content.substring(start, end)) as List;
|
||||||
|
|
||||||
|
// Build productName (lowercase) → (storeLabel, StoreProduct) lookup
|
||||||
|
// Key: "storeName — locationName|productname_lowercase"
|
||||||
|
// This avoids cross-store collisions if two stores have the same product name
|
||||||
|
final productLookup = <String, MapEntry<String, StoreProduct>>{};
|
||||||
|
for (final store in stores) {
|
||||||
|
final storeKey = '${store.tenantId}_${store.locationId}';
|
||||||
|
final products = _storeProducts[storeKey] ?? [];
|
||||||
|
final storeLabel = '${store.tenantName} — ${store.locationName}';
|
||||||
|
for (final p in products) {
|
||||||
|
final lookupKey = '$storeLabel|${p.productName.toLowerCase().trim()}';
|
||||||
|
productLookup[lookupKey] = MapEntry(storeLabel, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build MatchedItem list from AI response
|
||||||
|
matchedResults = aiItems.map((aiItem) {
|
||||||
|
final entry = parsed.firstWhere(
|
||||||
|
(e) =>
|
||||||
|
(e['aiItem'] as String).toLowerCase().trim() ==
|
||||||
|
aiItem.name.toLowerCase().trim(),
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
return MatchedItem(aiItem: aiItem, storeMatches: {});
|
||||||
|
}
|
||||||
|
|
||||||
|
final matchedNames = ((entry['matches'] as List?) ?? [])
|
||||||
|
.map((e) => (e as String).trim())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Group matched products by store name
|
||||||
|
final storeMatches = <String, List<StoreProduct>>{};
|
||||||
|
|
||||||
|
for (final matchedName in matchedNames) {
|
||||||
|
final matchedNameLower = matchedName.toLowerCase();
|
||||||
|
|
||||||
|
// Search across all stores for this product name
|
||||||
|
for (final store in stores) {
|
||||||
|
final storeKey = '${store.tenantId}_${store.locationId}';
|
||||||
|
final storeLabel = '${store.tenantName} — ${store.locationName}';
|
||||||
|
final lookupKey = '$storeLabel|$matchedNameLower';
|
||||||
|
|
||||||
|
final found = productLookup[lookupKey];
|
||||||
|
if (found != null) {
|
||||||
|
storeMatches
|
||||||
|
.putIfAbsent(found.key, () => [])
|
||||||
|
.add(found.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatchedItem(aiItem: aiItem, storeMatches: storeMatches);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('[StoreMatch] AI matching failed: $e\n$st');
|
||||||
|
// Fallback: return all items with no matches rather than crashing
|
||||||
|
matchedResults = aiItems
|
||||||
|
.map((item) => MatchedItem(aiItem: item, storeMatches: {}))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 4: Print results ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void _printMatchResults() {
|
||||||
|
debugPrint('\n${'═' * 60}');
|
||||||
|
debugPrint(' AI ITEM → STORE MATCH RESULTS (AI-powered)');
|
||||||
|
debugPrint('${'═' * 60}');
|
||||||
|
|
||||||
|
for (final result in matchedResults) {
|
||||||
|
debugPrint('\n▶ AI Item: "${result.aiItem.name}" '
|
||||||
|
'(${result.aiItem.quantity}) ${result.aiItem.emoji}');
|
||||||
|
|
||||||
|
if (!result.hasMatches) {
|
||||||
|
debugPrint(' ✗ No matching products found in any store.');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.storeMatches.forEach((storeName, products) {
|
||||||
|
debugPrint(' ✔ $storeName');
|
||||||
|
for (final p in products) {
|
||||||
|
final discount = p.discountValue > 0
|
||||||
|
? ' [${p.discountValue.toStringAsFixed(0)}% off → ₹${p.finalPrice.toStringAsFixed(2)}]'
|
||||||
|
: '';
|
||||||
|
final status = p.isAvailable ? '✅' : '⚠ outofstock';
|
||||||
|
debugPrint(
|
||||||
|
' • ${p.productName} | ₹${p.productCost}'
|
||||||
|
'$discount | ${p.productUnit} | $status',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('\n${'═' * 60}');
|
||||||
|
final matchedCount = matchedResults.where((r) => r.hasMatches).length;
|
||||||
|
debugPrint(
|
||||||
|
' SUMMARY: ${matchedCount}/${matchedResults.length} AI items matched '
|
||||||
|
'across ${stores.length} stores.',
|
||||||
|
);
|
||||||
|
debugPrint('${'═' * 60}\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
stores = [];
|
||||||
|
_storeProducts.clear();
|
||||||
|
matchedResults = [];
|
||||||
|
error = null;
|
||||||
|
loading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MatchedItem> get foundItems =>
|
||||||
|
matchedResults.where((r) => r.hasMatches).toList();
|
||||||
|
|
||||||
|
List<MatchedItem> get notFoundItems =>
|
||||||
|
matchedResults.where((r) => !r.hasMatches).toList();
|
||||||
|
}
|
||||||
295
lib/controllers/search_controller/search_controller.dart
Normal file
295
lib/controllers/search_controller/search_controller.dart
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:nearledaily/controllers/search_controller/product_match.dart';
|
||||||
|
import '../../modules/search/grocery_item.dart';
|
||||||
|
|
||||||
|
class GrocerySearchController extends ChangeNotifier {
|
||||||
|
// ─── Controllers & Focus ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
final TextEditingController textController = TextEditingController();
|
||||||
|
final FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
|
// ─── Animation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
late AnimationController cartBarController;
|
||||||
|
late Animation<Offset> cartBarSlide;
|
||||||
|
|
||||||
|
// ─── State ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
List<GroceryItem> items = [];
|
||||||
|
List<GroceryItem> cartItems = [];
|
||||||
|
bool loading = false;
|
||||||
|
bool hasSearched = false;
|
||||||
|
String lastQuery = '';
|
||||||
|
|
||||||
|
// ─── API ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static const String _apiKey =
|
||||||
|
'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';
|
||||||
|
|
||||||
|
// ─── Quick Chips ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
final List<Map<String, String>> quickChips = [
|
||||||
|
{'label': '🍛 Biryani', 'query': 'Biryani'},
|
||||||
|
{'label': '🍝 Pasta', 'query': 'Pasta'},
|
||||||
|
{'label': '🎂 Cake', 'query': 'Cake'},
|
||||||
|
{'label': '🥞 Pancakes', 'query': 'Pancakes'},
|
||||||
|
{'label': '🫘 Dal Tadka', 'query': 'Dal Tadka'},
|
||||||
|
{'label': '🥘 Idli Sambar', 'query': 'Idli Sambar'},
|
||||||
|
{'label': '🍕 Pizza', 'query': 'Pizza'},
|
||||||
|
{'label': '🍜 Noodles', 'query': 'Noodles'},
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<Map<String, String>> naturalChips = [
|
||||||
|
{
|
||||||
|
'label': '🤧 I have a cold — what should I eat?',
|
||||||
|
'query': 'I have a cold, what fruits or foods should I eat?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '💪 Post-workout nutrition',
|
||||||
|
'query': 'Post-workout recovery foods and snacks'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '🍽️ Biryani + Curd Rice together',
|
||||||
|
'query': 'I want to make biryani and also curd rice'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '🍳 Quick egg breakfast',
|
||||||
|
'query': 'Quick breakfast with eggs and bread'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Init / Dispose ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void init(TickerProvider vsync) {
|
||||||
|
// Register StoreMatchController if not already registered
|
||||||
|
if (!Get.isRegistered<StoreMatchController>()) {
|
||||||
|
Get.put(StoreMatchController());
|
||||||
|
}
|
||||||
|
initAnimations(vsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initAnimations(TickerProvider vsync) {
|
||||||
|
cartBarController = AnimationController(
|
||||||
|
vsync: vsync,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
cartBarSlide = Tween<Offset>(
|
||||||
|
begin: const Offset(0, 1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: cartBarController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeAll() {
|
||||||
|
textController.dispose();
|
||||||
|
focusNode.dispose();
|
||||||
|
cartBarController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Search ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Future<void> search(String query) async {
|
||||||
|
final q = query.trim();
|
||||||
|
if (q.isEmpty) return;
|
||||||
|
|
||||||
|
focusNode.unfocus();
|
||||||
|
loading = true;
|
||||||
|
hasSearched = true;
|
||||||
|
lastQuery = q;
|
||||||
|
items = [];
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse('https://api.groq.com/openai/v1/chat/completions'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $_apiKey',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'model': 'llama-3.1-8b-instant',
|
||||||
|
'max_tokens': 800,
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content': '''You are a smart product search engine for a super-app with grocery stores, restaurants, pharmacies, and general stores.
|
||||||
|
|
||||||
|
Your ONLY job is to return a raw JSON array. No markdown, no code fences, no explanations. Raw JSON only.
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
GOLDEN RULE — LITERAL FIRST:
|
||||||
|
If the user types a RAW INGREDIENT, PRODUCT, or ITEM NAME (e.g. "tomato", "milk", "onion", "shampoo", "paracetamol"),
|
||||||
|
return THAT EXACT ITEM as a grocery/store product. Do NOT transform it into a dish or recipe.
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
|
||||||
|
INTENT DETECTION — pick ONE mode:
|
||||||
|
|
||||||
|
MODE 1 — DIRECT PRODUCT SEARCH (user types a product/ingredient name):
|
||||||
|
Signals: single word or short phrase that IS a product (e.g. "tomato", "milk", "bread", "eggs", "rice")
|
||||||
|
Action: Return that exact product with 2–4 size/variant options only.
|
||||||
|
Do NOT suggest dishes made from it.
|
||||||
|
type: "grocery"
|
||||||
|
Example — query "tomato":
|
||||||
|
[
|
||||||
|
{"name": "Tomato", "quantity": "500g", "emoji": "🍅", "type": "grocery"},
|
||||||
|
{"name": "Tomato", "quantity": "1 kg", "emoji": "🍅", "type": "grocery"},
|
||||||
|
{"name": "Cherry Tomato", "quantity": "250g", "emoji": "🍅", "type": "grocery"}
|
||||||
|
]
|
||||||
|
|
||||||
|
MODE 2 — RESTAURANT ORDER (user wants a ready-made dish delivered):
|
||||||
|
Signals: dish name WITH food-order context, or words like "order", "hungry", "eat",
|
||||||
|
"food delivery", "i want to eat [dish]", "get me [dish]"
|
||||||
|
Action: Return the dish as 1–3 variants.
|
||||||
|
type: "restaurant"
|
||||||
|
Example — query "i want biryani":
|
||||||
|
[
|
||||||
|
{"name": "Chicken Biryani", "quantity": "1 portion", "emoji": "🍛", "type": "restaurant"},
|
||||||
|
{"name": "Veg Biryani", "quantity": "1 portion", "emoji": "🍚", "type": "restaurant"}
|
||||||
|
]
|
||||||
|
|
||||||
|
MODE 3 — COOK AT HOME (user wants to cook):
|
||||||
|
Signals: "cook", "recipe", "ingredients for", "how to make", "groceries for", "i will make"
|
||||||
|
Action: Return grocery ingredient list (6–12 items).
|
||||||
|
type: "grocery"
|
||||||
|
|
||||||
|
MODE 4 — SITUATION / NEED (health, mood, lifestyle):
|
||||||
|
Signals: "sick", "fever", "diet", "weight loss", "party", "snacks", "breakfast", "workout"
|
||||||
|
Action: Return 5–10 relevant products. Mix types if appropriate.
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
STRICT ACCURACY RULES:
|
||||||
|
- A product name is a product — never auto-upgrade it to a dish
|
||||||
|
- Only return items the user actually needs — no upsells, no extras
|
||||||
|
- If query is ambiguous between product vs dish, choose MODE 1 (product)
|
||||||
|
- "tomato" → tomatoes, NOT tomato rice or tomato sauce
|
||||||
|
- "chicken" → raw chicken cuts, NOT chicken curry or chicken burger
|
||||||
|
- "lemon" → lemons, NOT lemon juice or lemon cake
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
|
||||||
|
OUTPUT RULES:
|
||||||
|
- Raw JSON array only. Nothing else.
|
||||||
|
- Every item: exactly 4 keys — "name", "quantity", "emoji", "type"
|
||||||
|
- "type": only "restaurant" or "grocery"
|
||||||
|
- No duplicates. No nulls. No empty strings.
|
||||||
|
- Max 6 items total unless MODE 3 (ingredients list).''',
|
||||||
|
},
|
||||||
|
{'role': 'user', 'content': q},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception('API error ${res.statusCode}: ${res.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||||
|
final content = data['choices'][0]['message']['content'] as String;
|
||||||
|
|
||||||
|
final start = content.indexOf('[');
|
||||||
|
final end = content.lastIndexOf(']') + 1;
|
||||||
|
if (start == -1 || end == 0) {
|
||||||
|
throw Exception('No JSON array found in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final List parsed =
|
||||||
|
jsonDecode(content.substring(start, end)) as List;
|
||||||
|
|
||||||
|
final newItems = parsed
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) =>
|
||||||
|
GroceryItem.fromJson(e.key, e.value as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Preserve cart state for already-added items
|
||||||
|
for (final item in newItems) {
|
||||||
|
if (cartItems.any((c) => c.name == item.name)) {
|
||||||
|
item.inCart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items = newItems;
|
||||||
|
|
||||||
|
// ── Trigger store matching ──────────────────────────────────────────
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final storeCtrl = Get.find<StoreMatchController>();
|
||||||
|
await storeCtrl.matchAiItems(items);
|
||||||
|
} catch (e, st) {
|
||||||
|
print('[StoreMatch] Error during matching: $e');
|
||||||
|
print(' StackTrace: $st');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
print('[GrocerySearch] FormatException: $e');
|
||||||
|
} on http.ClientException catch (e) {
|
||||||
|
print('[GrocerySearch] Network error: $e');
|
||||||
|
} catch (e, st) {
|
||||||
|
print('[GrocerySearch] Unexpected error: $e');
|
||||||
|
print(' StackTrace: $st');
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ─── Cart ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void toggleCart(GroceryItem item) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
if (item.inCart) {
|
||||||
|
item.inCart = false;
|
||||||
|
cartItems.removeWhere((c) => c.id == item.id && c.name == item.name);
|
||||||
|
} else {
|
||||||
|
item.inCart = true;
|
||||||
|
cartItems.add(item);
|
||||||
|
}
|
||||||
|
cartItems.isNotEmpty
|
||||||
|
? cartBarController.forward()
|
||||||
|
: cartBarController.reverse();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAllToCart() {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
for (final item in items) {
|
||||||
|
if (!item.inCart) {
|
||||||
|
item.inCart = true;
|
||||||
|
cartItems.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cartBarController.forward();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get allInCart => items.isNotEmpty && items.every((i) => i.inCart);
|
||||||
|
|
||||||
|
void removeFromCart(GroceryItem item) {
|
||||||
|
item.inCart = false;
|
||||||
|
cartItems.removeWhere((c) => c.id == item.id && c.name == item.name);
|
||||||
|
if (cartItems.isEmpty) cartBarController.reverse();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void clearSearch() {
|
||||||
|
textController.clear();
|
||||||
|
hasSearched = false;
|
||||||
|
items = [];
|
||||||
|
lastQuery = '';
|
||||||
|
focusNode.requestFocus();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillSearch(String query) {
|
||||||
|
textController.text = query;
|
||||||
|
search(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
lib/core/services/ExitWrapper.dart
Normal file
0
lib/core/services/ExitWrapper.dart
Normal file
86
lib/core/services/vector_service.dart
Normal file
86
lib/core/services/vector_service.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
|
||||||
|
/// Holds the two vector embeddings: one for the cropped product image,
|
||||||
|
/// one for the OCR-extracted label text.
|
||||||
|
class VectorEmbeddingResult {
|
||||||
|
final List<double> imageVector;
|
||||||
|
final List<double> textVector;
|
||||||
|
|
||||||
|
const VectorEmbeddingResult({
|
||||||
|
required this.imageVector,
|
||||||
|
required this.textVector,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes vector embeddings entirely on-device — no backend call.
|
||||||
|
///
|
||||||
|
/// NOTE: these are lightweight placeholder embeddings (a downsampled
|
||||||
|
/// grayscale pixel vector for the image, a character-hashing vector for
|
||||||
|
/// the text), NOT a trained model like CLIP. They're deterministic and
|
||||||
|
/// good enough to wire up printing/plumbing now. Swap `_imageVector` /
|
||||||
|
/// `_textVector` for a real on-device model (e.g. a TFLite feature
|
||||||
|
/// extractor) or a backend call later without touching the call site.
|
||||||
|
class VectorService {
|
||||||
|
static const int _imageVectorSize = 64; // 8x8 downsampled grayscale
|
||||||
|
static const int _textVectorSize = 32; // hashed char buckets
|
||||||
|
|
||||||
|
static Future<VectorEmbeddingResult> getEmbeddings({
|
||||||
|
required String imagePath,
|
||||||
|
required String labelText,
|
||||||
|
}) async {
|
||||||
|
final imageVector = await _imageVector(imagePath);
|
||||||
|
final textVector = _textVector(labelText);
|
||||||
|
|
||||||
|
return VectorEmbeddingResult(
|
||||||
|
imageVector: imageVector,
|
||||||
|
textVector: textVector,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downsamples the image to an 8x8 grayscale grid and flattens it into
|
||||||
|
/// a normalized (0-1) vector of length [_imageVectorSize].
|
||||||
|
static Future<List<double>> _imageVector(String imagePath) async {
|
||||||
|
final bytes = await File(imagePath).readAsBytes();
|
||||||
|
final decoded = img.decodeImage(bytes);
|
||||||
|
if (decoded == null) {
|
||||||
|
return List<double>.filled(_imageVectorSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final side = math.sqrt(_imageVectorSize).round(); // 8
|
||||||
|
final resized = img.copyResize(decoded, width: side, height: side);
|
||||||
|
final gray = img.grayscale(resized);
|
||||||
|
|
||||||
|
final vector = <double>[];
|
||||||
|
for (var y = 0; y < side; y++) {
|
||||||
|
for (var x = 0; x < side; x++) {
|
||||||
|
final pixel = gray.getPixel(x, y);
|
||||||
|
vector.add(pixel.r / 255.0); // grayscale => r == g == b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple character-hashing bag-of-characters vector, normalized so
|
||||||
|
/// values sum to 1 (empty text => all zeros).
|
||||||
|
static List<double> _textVector(String text) {
|
||||||
|
final vector = List<double>.filled(_textVectorSize, 0);
|
||||||
|
final normalized = text.toLowerCase();
|
||||||
|
if (normalized.isEmpty) return vector;
|
||||||
|
|
||||||
|
for (final rune in normalized.runes) {
|
||||||
|
final bucket = rune % _textVectorSize;
|
||||||
|
vector[bucket] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final total = vector.fold<double>(0, (sum, v) => sum + v);
|
||||||
|
if (total > 0) {
|
||||||
|
for (var i = 0; i < vector.length; i++) {
|
||||||
|
vector[i] = vector[i] / total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vector;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,8 +68,10 @@ class LoginProvider {
|
|||||||
CustomerFullView? profile;
|
CustomerFullView? profile;
|
||||||
final customDio = CustomDio();
|
final customDio = CustomDio();
|
||||||
final url = "${ApiConstants.fetchProfile}customerid=$customerId&contactno=''";
|
final url = "${ApiConstants.fetchProfile}customerid=$customerId&contactno=''";
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
print(url);
|
||||||
final response = await customDio.getData(url,
|
final response = await customDio.getData(url,
|
||||||
headers: {
|
headers: {
|
||||||
'x-hasura-admin-secret': 'nearle-admin-secret',
|
'x-hasura-admin-secret': 'nearle-admin-secret',
|
||||||
|
|||||||
316
lib/features/Products/product_card.dart
Normal file
316
lib/features/Products/product_card.dart
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../modules/search_model/search_model.dart';
|
||||||
|
import '../../widgets/fav_button.dart';
|
||||||
|
|
||||||
|
class ProductCard extends StatefulWidget {
|
||||||
|
final ProductItem item;
|
||||||
|
final VoidCallback? onAddTap;
|
||||||
|
final VoidCallback? onIncrementTap;
|
||||||
|
final VoidCallback? onDecrementTap;
|
||||||
|
final int quantity; // 0 = show "Add", >0 = show stepper
|
||||||
|
|
||||||
|
const ProductCard({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.onAddTap,
|
||||||
|
this.onIncrementTap,
|
||||||
|
this.onDecrementTap,
|
||||||
|
this.quantity = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProductCard> createState() => _ProductCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductCardState extends State<ProductCard> {
|
||||||
|
bool _isFav = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_isFav = widget.item.isFavorite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final item = widget.item;
|
||||||
|
|
||||||
|
final original = double.tryParse(
|
||||||
|
item.originalPrice.replaceAll(RegExp(r'[^\d.]'), ''),
|
||||||
|
) ??
|
||||||
|
0;
|
||||||
|
final current = double.tryParse(
|
||||||
|
item.price.replaceAll(RegExp(r'[^\d.]'), ''),
|
||||||
|
) ??
|
||||||
|
0;
|
||||||
|
final discount = original > 0
|
||||||
|
? ((original - current) / original * 100).round()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// ── Image area ──
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// Product image
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: item.image,
|
||||||
|
height: 100,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (_, __) => const SizedBox.shrink(),
|
||||||
|
errorWidget: (context, url, error) => Container(
|
||||||
|
height: 140,
|
||||||
|
color: const Color(0xFFF3F0FF),
|
||||||
|
child: Image.network('https://i0.wp.com/www.chitrasfoodbook.com/wp-content/uploads/2014/08/vegetables-for-salad.jpg?w=1200&ssl=1'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Discount badge — top left
|
||||||
|
if (discount > 0)
|
||||||
|
Positioned(
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF662582),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$discount% OFF',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Favourite button — top right
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: FavButton(
|
||||||
|
size: 32,
|
||||||
|
initialValue: _isFav,
|
||||||
|
onChanged: (val) => setState(() => _isFav = val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Add / Stepper button — bottom right ON image
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
right: 10,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
switchInCurve: Curves.easeOutBack,
|
||||||
|
switchOutCurve: Curves.easeIn,
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: widget.quantity <= 0
|
||||||
|
? GestureDetector(
|
||||||
|
key: const ValueKey('add_button'), // important!
|
||||||
|
onTap: widget.onAddTap,
|
||||||
|
child: Container(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.12),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
key: const ValueKey('counter_row'), // important!
|
||||||
|
height: 34,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.12),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: widget.onDecrementTap,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.remove_rounded,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'${widget.quantity}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: widget.onIncrementTap,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Info area ──
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// Category
|
||||||
|
Text(
|
||||||
|
item.category,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
|
||||||
|
// Product name
|
||||||
|
Text(
|
||||||
|
item.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Rating row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.star_rounded,
|
||||||
|
size: 13,
|
||||||
|
color: Color(0xFFFFC107),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(
|
||||||
|
'${item.rating}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(
|
||||||
|
'(${item.reviewCount})',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: Color(0xFF9E9E9E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Price row — no button here anymore
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.price,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
// if (discount > 0)
|
||||||
|
// Text(
|
||||||
|
// item.originalPrice,
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 11,
|
||||||
|
// color: Color(0xFF9E9E9E),
|
||||||
|
// decoration: TextDecoration.lineThrough,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
120
lib/features/Store/presentation/controller/store_controller.dart
Normal file
120
lib/features/Store/presentation/controller/store_controller.dart
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
/// Manages all scroll, cart-bar visibility, back-to-top, and
|
||||||
|
/// network-retry logic for StoreViewPage.
|
||||||
|
class StoreViewController extends GetxController {
|
||||||
|
// ── Scroll ──────────────────────────────────────────────
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
|
double scrollOffset = 0.0;
|
||||||
|
bool showBackToTop = false;
|
||||||
|
bool hideCartBar = false;
|
||||||
|
|
||||||
|
Timer? _cartTimer;
|
||||||
|
|
||||||
|
// ── Mini-cart ────────────────────────────────────────────
|
||||||
|
RxBool showMiniCart = false.obs;
|
||||||
|
|
||||||
|
// ── Search ───────────────────────────────────────────────
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
|
||||||
|
// ── Misc ─────────────────────────────────────────────────
|
||||||
|
var selectedCategoryId = 0.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
scrollController.addListener(_onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
scrollController.removeListener(_onScroll);
|
||||||
|
scrollController.dispose();
|
||||||
|
searchController.dispose();
|
||||||
|
_cartTimer?.cancel();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Scroll handler (used by the ScrollController listener) ──
|
||||||
|
void _onScroll() {
|
||||||
|
scrollOffset = scrollController.offset;
|
||||||
|
showBackToTop = scrollController.offset > 300;
|
||||||
|
update(['scroll']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notification-based handler (NestedScrollView body) ──────
|
||||||
|
bool handleScrollNotification(ScrollNotification notification) {
|
||||||
|
if (notification is ScrollUpdateNotification &&
|
||||||
|
notification.metrics.axis == Axis.vertical) {
|
||||||
|
final double pixels = notification.metrics.pixels;
|
||||||
|
final bool isScrollingUp = pixels < scrollOffset;
|
||||||
|
|
||||||
|
scrollOffset = pixels;
|
||||||
|
|
||||||
|
if (isScrollingUp && pixels > 300) {
|
||||||
|
showBackToTop = true;
|
||||||
|
} else if (pixels <= 300 || !isScrollingUp) {
|
||||||
|
showBackToTop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide cart bar immediately on scroll
|
||||||
|
if (!hideCartBar) {
|
||||||
|
hideCartBar = true;
|
||||||
|
showMiniCart.value = false;
|
||||||
|
update(['cartBar']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-show cart bar after scroll stops
|
||||||
|
_cartTimer?.cancel();
|
||||||
|
_cartTimer = Timer(const Duration(milliseconds: 250), () {
|
||||||
|
if (hideCartBar) {
|
||||||
|
hideCartBar = false;
|
||||||
|
update(['cartBar']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
update(['scroll']);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Background colour based on scroll position ───────────────
|
||||||
|
Color backgroundColorFor(List<Color> colors) {
|
||||||
|
if (colors.isEmpty) return Colors.white;
|
||||||
|
return colors[(scrollOffset ~/ 300) % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Scroll to top ─────────────────────────────────────────────
|
||||||
|
void scrollToTop() {
|
||||||
|
scrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Network retry ─────────────────────────────────────────────
|
||||||
|
Future<void> retryNetworkCall(Future<void> Function() onSuccess) async {
|
||||||
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
|
|
||||||
|
if (connectivityResult == ConnectivityResult.mobile ||
|
||||||
|
connectivityResult == ConnectivityResult.wifi) {
|
||||||
|
debugPrint('✅ Internet available, retrying...');
|
||||||
|
await onSuccess();
|
||||||
|
} else {
|
||||||
|
debugPrint('❌ Still no internet connection');
|
||||||
|
Get.snackbar(
|
||||||
|
'No Internet',
|
||||||
|
'Please check your connection and try again.',
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
colorText: Colors.black,
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
454
lib/features/Store/presentation/screens/store_view.dart
Normal file
454
lib/features/Store/presentation/screens/store_view.dart
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart';
|
||||||
|
|
||||||
|
import '../../../../constants/color_constants.dart';
|
||||||
|
import '../../../../constants/font_constants.dart';
|
||||||
|
import '../../../../controllers/account_controller/profile.dart';
|
||||||
|
import '../../../../controllers/cart_controller/cart.dart';
|
||||||
|
import '../../../../controllers/dashboard_controller/dashboard_controller.dart';
|
||||||
|
import '../../../../controllers/tenant_list/tenant_controller.dart';
|
||||||
|
import '../../../../service/firebase_analytics/analytics_service.dart';
|
||||||
|
import '../../../../view/dashboard_view/tenant_profile.dart';
|
||||||
|
import '../../../../view/product/tenant_products.dart';
|
||||||
|
import '../../../../widgets/text_widget.dart';
|
||||||
|
import '../controller/store_controller.dart';
|
||||||
|
import '../widgets/store_widgets.dart' hide ZoomIconButton;
|
||||||
|
|
||||||
|
|
||||||
|
class StoreViewPage extends StatefulWidget {
|
||||||
|
const StoreViewPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StoreViewPage> createState() => _StoreViewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StoreViewPageState extends State<StoreViewPage> {
|
||||||
|
// ── Controllers ──────────────────────────────────────────────
|
||||||
|
final StoreViewController storeCtrl = Get.put(StoreViewController());
|
||||||
|
final TenantController tenantCtrl = Get.put(TenantController());
|
||||||
|
final CartController cartCtrl = Get.put(CartController());
|
||||||
|
|
||||||
|
bool status = true;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.white,
|
||||||
|
statusBarIconBrightness: Brightness.dark,
|
||||||
|
statusBarBrightness: Brightness.light,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
body: GetBuilder<StoreViewController>(
|
||||||
|
id: 'scroll',
|
||||||
|
builder: (ctrl) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
color: ctrl.backgroundColorFor(ColorConstants.bgColors),
|
||||||
|
child: NestedScrollViewPlus(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
controller: ctrl.scrollController,
|
||||||
|
headerSliverBuilder: (context, _) => [
|
||||||
|
CupertinoSliverRefreshControl(
|
||||||
|
onRefresh: () async {
|
||||||
|
await tenantCtrl.loadTenants();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: ctrl.handleScrollNotification,
|
||||||
|
child: CustomScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics(),
|
||||||
|
),
|
||||||
|
slivers: [
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: 12)),
|
||||||
|
|
||||||
|
// ── Section header ──────────────────────────────
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: ReusableTextWidget(
|
||||||
|
text: "Near by Stores",
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Store list ──────────────────────────────────
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 0),
|
||||||
|
sliver: Obx(() {
|
||||||
|
// No internet
|
||||||
|
if (!tenantCtrl.isConnected.value) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: NoInternetWidget(
|
||||||
|
onRetry: () => ctrl.retryNetworkCall(
|
||||||
|
() async => await tenantCtrl.loadTenants(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading
|
||||||
|
if (tenantCtrl.isLoading.value) {
|
||||||
|
return const StoreListShimmer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty
|
||||||
|
if (tenantCtrl.tenants.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: NoStoresFound(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store list
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final item = tenantCtrl.tenants[index];
|
||||||
|
return ZoomOnTap(
|
||||||
|
onTap: () => _openStore(item),
|
||||||
|
child: _StoreCard(
|
||||||
|
item: item,
|
||||||
|
status: status,
|
||||||
|
onInfoTap: () =>
|
||||||
|
Get.to(StoreOverviewScreen()),
|
||||||
|
onCategoryArrowTap: () =>
|
||||||
|
_openStore(item),
|
||||||
|
onSubcategoryTap: (_) =>
|
||||||
|
_openStore(item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: tenantCtrl.tenants.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: 220)),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/nearle_copyrights.png'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────
|
||||||
|
void _openStore(dynamic item) async {
|
||||||
|
Get.to(() => ProductsScreen(
|
||||||
|
tenantId: item.tenantid!,
|
||||||
|
locationId: item.locationid!,
|
||||||
|
categoryId: item.categoryid!,
|
||||||
|
tenantName: item.tenantname!,
|
||||||
|
locationname: item.locationname!,
|
||||||
|
tenantLocation: item.suburb!,
|
||||||
|
tenantImage: item.tenantimage!,
|
||||||
|
tenantloc: item.locationid!,
|
||||||
|
subCategoryName: '',
|
||||||
|
));
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'store_viewed',
|
||||||
|
parameters: {
|
||||||
|
'store_id': item.tenantid!,
|
||||||
|
'store_name': item.tenantname!,
|
||||||
|
'locatiom': item.locationname!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// Private store card widget (lives here because it needs item data)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
class _StoreCard extends StatelessWidget {
|
||||||
|
final dynamic item;
|
||||||
|
final bool status;
|
||||||
|
final VoidCallback onInfoTap;
|
||||||
|
final VoidCallback onCategoryArrowTap;
|
||||||
|
final void Function(dynamic subcategory) onSubcategoryTap;
|
||||||
|
|
||||||
|
const _StoreCard({
|
||||||
|
required this.item,
|
||||||
|
required this.status,
|
||||||
|
required this.onInfoTap,
|
||||||
|
required this.onCategoryArrowTap,
|
||||||
|
required this.onSubcategoryTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
double _rs(BuildContext context, double size) =>
|
||||||
|
size * (MediaQuery.of(context).size.width / 390);
|
||||||
|
|
||||||
|
double _rh(BuildContext context, double size) =>
|
||||||
|
size * (MediaQuery.of(context).size.height / 844);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12, top: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFFE1E5EA), width: 0.55),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.10),
|
||||||
|
spreadRadius: 0,
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.06),
|
||||||
|
spreadRadius: 0,
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// ── Header row ──────────────────────────────────────
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: item.tenantname!,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
fontSize: _rs(context, 23),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.location_on,
|
||||||
|
color: Colors.grey, size: _rs(context, 11)),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: item.locationname!,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
fontSize: _rs(context, 10),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ZoomIconButton(
|
||||||
|
onTap: onInfoTap,
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
size: 25,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Body: closed or open ─────────────────────────────
|
||||||
|
status == false
|
||||||
|
? const ClosedStoreWidget()
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
// Banner
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
height: _rh(context, 150),
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: (item.tenantbanner != null &&
|
||||||
|
item.tenantbanner!.isNotEmpty)
|
||||||
|
? Image.network(
|
||||||
|
item.tenantbanner!,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
errorBuilder: (_, __, ___) => Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.image_not_supported_outlined,
|
||||||
|
size: _rs(context, 40),
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Image.network(
|
||||||
|
'https://img.freepik.com/free-psd/healthy-eating-lifestyle-banner-template_23-2149087275.jpg',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Category row
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'Category',
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
fontSize: _rs(context, 15),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onCategoryArrowTap,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_circle_right_outlined,
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
size: _rs(context, 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Subcategories
|
||||||
|
SizedBox(
|
||||||
|
height: _rh(context, 160),
|
||||||
|
child: (item.subcategories != null &&
|
||||||
|
item.subcategories!.isNotEmpty)
|
||||||
|
? ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
itemCount: item.subcategories!.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = item.subcategories![index];
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () => onSubcategoryTap(product),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
bottom: 14, right: 12, top: 2),
|
||||||
|
height: _rh(context, 180),
|
||||||
|
width: _rs(context, 120),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.black12, width: 0.20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color:
|
||||||
|
Colors.black.withOpacity(0.10),
|
||||||
|
spreadRadius: 0,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: _rh(context, 12)),
|
||||||
|
ClipOval(
|
||||||
|
child: Image.network(
|
||||||
|
product.image ?? '',
|
||||||
|
width: _rs(context, 80),
|
||||||
|
height: _rs(context, 80),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
Icon(
|
||||||
|
Icons.image_not_supported,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: _rs(context, 30),
|
||||||
|
),
|
||||||
|
loadingBuilder:
|
||||||
|
(_, child, progress) {
|
||||||
|
if (progress == null)
|
||||||
|
return child;
|
||||||
|
return const Center(
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: Colors.grey),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8),
|
||||||
|
child: Center(
|
||||||
|
child: ReusableTextWidget(
|
||||||
|
text: product.subcatname!,
|
||||||
|
color: Colors.black
|
||||||
|
.withOpacity(0.7),
|
||||||
|
fontFamily:
|
||||||
|
FontConstants.fontFamily,
|
||||||
|
fontSize: _rs(context, 12),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: _rh(context, 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
'No products available',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
425
lib/features/Store/presentation/widgets/store_widgets.dart
Normal file
425
lib/features/Store/presentation/widgets/store_widgets.dart
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
|
import '../../../../constants/color_constants.dart';
|
||||||
|
import '../../../../constants/font_constants.dart';
|
||||||
|
import '../../../../widgets/text_widget.dart';
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Zoom-on-tap wrapper
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class ZoomOnTap extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const ZoomOnTap({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ZoomOnTap> createState() => _ZoomOnTapState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZoomOnTapState extends State<ZoomOnTap> {
|
||||||
|
double _scale = 1.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTapDown: (_) => setState(() => _scale = 0.96),
|
||||||
|
onTapUp: (_) {
|
||||||
|
setState(() => _scale = 1.0);
|
||||||
|
widget.onTap();
|
||||||
|
},
|
||||||
|
onTapCancel: () => setState(() => _scale = 1.0),
|
||||||
|
child: AnimatedScale(
|
||||||
|
scale: _scale,
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Zoom icon button
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class ZoomIconButton extends StatefulWidget {
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final IconData icon;
|
||||||
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const ZoomIconButton({
|
||||||
|
super.key,
|
||||||
|
required this.onTap,
|
||||||
|
required this.icon,
|
||||||
|
this.size = 24,
|
||||||
|
this.color = Colors.black87,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ZoomIconButton> createState() => _ZoomIconButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZoomIconButtonState extends State<ZoomIconButton> {
|
||||||
|
double _scale = 1.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTapDown: (_) => setState(() => _scale = 0.85),
|
||||||
|
onTapUp: (_) {
|
||||||
|
setState(() => _scale = 1.0);
|
||||||
|
widget.onTap();
|
||||||
|
},
|
||||||
|
onTapCancel: () => setState(() => _scale = 1.0),
|
||||||
|
child: AnimatedScale(
|
||||||
|
scale: _scale,
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: Icon(widget.icon, size: widget.size, color: widget.color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Search bar
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class StoreSearchBar extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String hint;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const StoreSearchBar({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.hint = 'Search',
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 48,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.search, color: Colors.grey.shade600),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
onTap: onTap,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// No internet widget
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class NoInternetWidget extends StatelessWidget {
|
||||||
|
final VoidCallback onRetry;
|
||||||
|
|
||||||
|
const NoInternetWidget({super.key, required this.onRetry});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Lottie.asset(
|
||||||
|
'assets/lotties/no_internet.json',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'No Internet Connection',
|
||||||
|
color: Colors.grey[700]!,
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: onRetry,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: ColorConstants.primaryColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Retry',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// No stores found
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class NoStoresFound extends StatelessWidget {
|
||||||
|
const NoStoresFound({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Lottie.asset('assets/lotties/QR Code scan on phone.json'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'We don\'t deliver to this location yet. Try browsing nearby stores instead.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Closed store UI
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class ClosedStoreWidget extends StatelessWidget {
|
||||||
|
const ClosedStoreWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Lottie.asset(
|
||||||
|
'assets/lotties/shop.json',
|
||||||
|
height: 140,
|
||||||
|
repeat: true,
|
||||||
|
animate: true,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Store is currently closed',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'Please come back later',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// List shimmer (loading placeholder)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class StoreListShimmer extends StatelessWidget {
|
||||||
|
const StoreListShimmer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey.shade300,
|
||||||
|
highlightColor: Colors.grey.shade100,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 160,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: MediaQuery.of(context).size.width * 0.6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: MediaQuery.of(context).size.width * 0.4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: 5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Grid shimmer (subcategory loading placeholder)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class SubcategoryGridShimmer extends StatelessWidget {
|
||||||
|
const SubcategoryGridShimmer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
double maxCrossAxisExtent = 250;
|
||||||
|
if (screenWidth > 1200) {
|
||||||
|
maxCrossAxisExtent = 300;
|
||||||
|
} else if (screenWidth > 800) {
|
||||||
|
maxCrossAxisExtent = 280;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverGrid(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10, left: 10, right: 10),
|
||||||
|
child: Container(
|
||||||
|
height: 140,
|
||||||
|
width: 140,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||||
|
child: Container(height: 20, width: 100, color: Colors.white),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 35,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: 6,
|
||||||
|
),
|
||||||
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisExtent: 230,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1934
lib/features/category/prestation/screens/category.dart
Normal file
1934
lib/features/category/prestation/screens/category.dart
Normal file
File diff suppressed because it is too large
Load Diff
243
lib/features/dashboard/data/grocery_product_model.dart
Normal file
243
lib/features/dashboard/data/grocery_product_model.dart
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// Models for:
|
||||||
|
// GET /live/api/v1/mob/products/getproductsbysubcategory
|
||||||
|
// ?categoryid=&tenantid=&locationid=
|
||||||
|
//
|
||||||
|
|
||||||
|
// Mirrors the response shape exactly (field names/casing kept as-is, e.g.
|
||||||
|
// the API's inconsistent "Subcategoryname" on the product object vs.
|
||||||
|
// "subcategoryname" on the section object).
|
||||||
|
|
||||||
|
class GroceryProductsResponse {
|
||||||
|
final int code;
|
||||||
|
final GroceryData data;
|
||||||
|
final String message;
|
||||||
|
final bool status;
|
||||||
|
|
||||||
|
GroceryProductsResponse({
|
||||||
|
required this.code,
|
||||||
|
required this.data,
|
||||||
|
required this.message,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GroceryProductsResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GroceryProductsResponse(
|
||||||
|
code: _asInt(json['code']),
|
||||||
|
data: GroceryData.fromJson(
|
||||||
|
(json['data'] as Map<String, dynamic>?) ?? const {}),
|
||||||
|
message: (json['message'] as String?) ?? '',
|
||||||
|
status: (json['status'] as bool?) ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroceryData {
|
||||||
|
final String address;
|
||||||
|
final String city;
|
||||||
|
final List<GrocerySubcategory> details;
|
||||||
|
final String licenseno;
|
||||||
|
final String locationname;
|
||||||
|
final String pickuplat;
|
||||||
|
final int pickuplocationid;
|
||||||
|
final String pickuplong;
|
||||||
|
final String postcode;
|
||||||
|
final String primarycontact;
|
||||||
|
final String primaryemail;
|
||||||
|
final String suburb;
|
||||||
|
final String tenantname;
|
||||||
|
|
||||||
|
GroceryData({
|
||||||
|
required this.address,
|
||||||
|
required this.city,
|
||||||
|
required this.details,
|
||||||
|
required this.licenseno,
|
||||||
|
required this.locationname,
|
||||||
|
required this.pickuplat,
|
||||||
|
required this.pickuplocationid,
|
||||||
|
required this.pickuplong,
|
||||||
|
required this.postcode,
|
||||||
|
required this.primarycontact,
|
||||||
|
required this.primaryemail,
|
||||||
|
required this.suburb,
|
||||||
|
required this.tenantname,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GroceryData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GroceryData(
|
||||||
|
address: (json['address'] as String?) ?? '',
|
||||||
|
city: (json['city'] as String?) ?? '',
|
||||||
|
details: ((json['details'] as List<dynamic>?) ?? const [])
|
||||||
|
.map((e) => GrocerySubcategory.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
licenseno: (json['licenseno'] as String?) ?? '',
|
||||||
|
locationname: (json['locationname'] as String?) ?? '',
|
||||||
|
pickuplat: (json['pickuplat'] as String?) ?? '',
|
||||||
|
pickuplocationid: _asInt(json['pickuplocationid']),
|
||||||
|
pickuplong: (json['pickuplong'] as String?) ?? '',
|
||||||
|
postcode: (json['postcode'] as String?) ?? '',
|
||||||
|
primarycontact: (json['primarycontact'] as String?) ?? '',
|
||||||
|
primaryemail: (json['primaryemail'] as String?) ?? '',
|
||||||
|
suburb: (json['suburb'] as String?) ?? '',
|
||||||
|
tenantname: (json['tenantname'] as String?) ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One row in `details`, e.g. "Vegetables & Fruits", "Dairy, Deli & Egg".
|
||||||
|
class GrocerySubcategory {
|
||||||
|
final int subcategoryId;
|
||||||
|
final String subcategoryName;
|
||||||
|
final String image;
|
||||||
|
final List<GroceryProduct> products;
|
||||||
|
|
||||||
|
GrocerySubcategory({
|
||||||
|
required this.subcategoryId,
|
||||||
|
required this.subcategoryName,
|
||||||
|
required this.image,
|
||||||
|
required this.products,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GrocerySubcategory.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GrocerySubcategory(
|
||||||
|
subcategoryId: _asInt(json['subcategoryid']),
|
||||||
|
subcategoryName: (json['subcategoryname'] as String?) ?? 'Other',
|
||||||
|
image: (json['image'] as String?) ?? '',
|
||||||
|
products: ((json['products'] as List<dynamic>?) ?? const [])
|
||||||
|
.map((e) => GroceryProduct.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroceryProduct {
|
||||||
|
final int productId;
|
||||||
|
final int applocationId;
|
||||||
|
final int productLocationId;
|
||||||
|
final int tenantId;
|
||||||
|
final int categoryId;
|
||||||
|
final String categoryName;
|
||||||
|
final int subcategoryId;
|
||||||
|
// Note: the API returns this capitalized on the product object.
|
||||||
|
final String subcategoryNameOnProduct;
|
||||||
|
final int discountId;
|
||||||
|
final num discountValue;
|
||||||
|
final String productName;
|
||||||
|
final String productDesc;
|
||||||
|
final String? productSku;
|
||||||
|
final String? productBrand;
|
||||||
|
final String productUnit;
|
||||||
|
final String unitValue;
|
||||||
|
final String? topPicks;
|
||||||
|
final num productCost;
|
||||||
|
final num? taxAmount;
|
||||||
|
final num? taxPercent;
|
||||||
|
final num productTax;
|
||||||
|
final int productStock;
|
||||||
|
final int productCombo;
|
||||||
|
final int variants;
|
||||||
|
final int quantity;
|
||||||
|
final num? retailPrice;
|
||||||
|
final num? diffPrice;
|
||||||
|
final num? diffPercent;
|
||||||
|
final int approve;
|
||||||
|
final String productStatus;
|
||||||
|
final String? productImage;
|
||||||
|
|
||||||
|
GroceryProduct({
|
||||||
|
required this.productId,
|
||||||
|
required this.applocationId,
|
||||||
|
required this.productLocationId,
|
||||||
|
required this.tenantId,
|
||||||
|
required this.categoryId,
|
||||||
|
required this.categoryName,
|
||||||
|
required this.subcategoryId,
|
||||||
|
required this.subcategoryNameOnProduct,
|
||||||
|
required this.discountId,
|
||||||
|
required this.discountValue,
|
||||||
|
required this.productName,
|
||||||
|
required this.productDesc,
|
||||||
|
this.productSku,
|
||||||
|
this.productBrand,
|
||||||
|
required this.productUnit,
|
||||||
|
required this.unitValue,
|
||||||
|
this.topPicks,
|
||||||
|
required this.productCost,
|
||||||
|
this.taxAmount,
|
||||||
|
this.taxPercent,
|
||||||
|
required this.productTax,
|
||||||
|
required this.productStock,
|
||||||
|
required this.productCombo,
|
||||||
|
required this.variants,
|
||||||
|
required this.quantity,
|
||||||
|
this.retailPrice,
|
||||||
|
this.diffPrice,
|
||||||
|
this.diffPercent,
|
||||||
|
required this.approve,
|
||||||
|
required this.productStatus,
|
||||||
|
this.productImage,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Selling price shown to the customer: retail price if set, else cost.
|
||||||
|
num get displayPrice => retailPrice ?? productCost;
|
||||||
|
|
||||||
|
/// Struck-through "was" price — only meaningful when the API sent a
|
||||||
|
/// positive diffprice (an actual discount amount).
|
||||||
|
num get displayOriginalPrice =>
|
||||||
|
(diffPrice != null && diffPrice! > 0) ? displayPrice + diffPrice! : displayPrice;
|
||||||
|
|
||||||
|
bool get isInStock => productStatus.toLowerCase() == 'available';
|
||||||
|
|
||||||
|
bool get isActive => productStatus.toLowerCase() != 'inactive';
|
||||||
|
|
||||||
|
factory GroceryProduct.fromJson(Map<String, dynamic> json) {
|
||||||
|
return GroceryProduct(
|
||||||
|
productId: _asInt(json['productid']),
|
||||||
|
applocationId: _asInt(json['applocationid']),
|
||||||
|
productLocationId: _asInt(json['productlocationid']),
|
||||||
|
tenantId: _asInt(json['tenantid']),
|
||||||
|
categoryId: _asInt(json['categoryid']),
|
||||||
|
categoryName: (json['categoryname'] as String?) ?? '',
|
||||||
|
subcategoryId: _asInt(json['subcategoryid']),
|
||||||
|
subcategoryNameOnProduct: (json['Subcategoryname'] as String?) ?? '',
|
||||||
|
discountId: _asInt(json['discountid']),
|
||||||
|
discountValue: _asNum(json['discountvalue']),
|
||||||
|
productName: (json['productname'] as String?) ?? 'Product',
|
||||||
|
productDesc: (json['productdesc'] as String?) ?? '',
|
||||||
|
productSku: json['productsku'] as String?,
|
||||||
|
productBrand: json['productbrand'] as String?,
|
||||||
|
productUnit: (json['productunit'] as String?) ?? '',
|
||||||
|
unitValue: (json['unitvalue'] as String?) ?? '',
|
||||||
|
topPicks: json['toppicks'] as String?,
|
||||||
|
productCost: _asNum(json['productcost']),
|
||||||
|
taxAmount: json['taxamount'] == null ? null : _asNum(json['taxamount']),
|
||||||
|
taxPercent:
|
||||||
|
json['taxpercent'] == null ? null : _asNum(json['taxpercent']),
|
||||||
|
productTax: _asNum(json['producttax']),
|
||||||
|
productStock: _asInt(json['productstock']),
|
||||||
|
productCombo: _asInt(json['productcombo']),
|
||||||
|
variants: _asInt(json['variants']),
|
||||||
|
quantity: _asInt(json['quantity']),
|
||||||
|
retailPrice:
|
||||||
|
json['retailprice'] == null ? null : _asNum(json['retailprice']),
|
||||||
|
diffPrice: json['diffprice'] == null ? null : _asNum(json['diffprice']),
|
||||||
|
diffPercent:
|
||||||
|
json['diffpercent'] == null ? null : _asNum(json['diffpercent']),
|
||||||
|
approve: _asInt(json['approve']),
|
||||||
|
productStatus: (json['productstatus'] as String?) ?? '',
|
||||||
|
productImage: json['productimage'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _asInt(dynamic v) {
|
||||||
|
if (v == null) return 0;
|
||||||
|
if (v is int) return v;
|
||||||
|
if (v is num) return v.toInt();
|
||||||
|
return int.tryParse(v.toString()) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
num _asNum(dynamic v) {
|
||||||
|
if (v == null) return 0;
|
||||||
|
if (v is num) return v;
|
||||||
|
return num.tryParse(v.toString()) ?? 0;
|
||||||
|
}
|
||||||
89
lib/features/dashboard/domain/repo/grocery_repository.dart
Normal file
89
lib/features/dashboard/domain/repo/grocery_repository.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
// TODO: point this at wherever your real `Product` model (the one used by
|
||||||
|
// CartController.addToCart) actually lives.
|
||||||
|
import '../../../../modules/product/product.dart';
|
||||||
|
|
||||||
|
/// One subcategory row returned by the API, e.g. "Vegetables & Fruits",
|
||||||
|
/// holding the real `Product` model straight from the API — the same type
|
||||||
|
/// CartController.addToCart expects, so no adapter/mapping is needed to
|
||||||
|
/// add a grocery item to the cart.
|
||||||
|
class GrocerySubcategorySection {
|
||||||
|
final int subcategoryId;
|
||||||
|
final String subcategoryName;
|
||||||
|
final String image;
|
||||||
|
final List<Product> products;
|
||||||
|
|
||||||
|
GrocerySubcategorySection({
|
||||||
|
required this.subcategoryId,
|
||||||
|
required this.subcategoryName,
|
||||||
|
required this.image,
|
||||||
|
required this.products,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroceryRepository {
|
||||||
|
static const String _baseUrl =
|
||||||
|
'https://fiesta.nearle.app/live/api/v1/mob/products/getproductsbysubcategory';
|
||||||
|
|
||||||
|
/// Fetches grocery products for the given category/tenant/location and
|
||||||
|
/// returns them grouped by subcategory, in the order the API returns them.
|
||||||
|
///
|
||||||
|
/// Throws an [Exception] with a readable message on failure so the caller
|
||||||
|
/// can show a retry UI.
|
||||||
|
Future<List<GrocerySubcategorySection>> fetchGroceryProducts({
|
||||||
|
required int categoryId,
|
||||||
|
required int tenantId,
|
||||||
|
required int locationId,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'$_baseUrl?categoryid=$categoryId&tenantid=$tenantId&locationid=$locationId',
|
||||||
|
);
|
||||||
|
|
||||||
|
late final http.Response response;
|
||||||
|
try {
|
||||||
|
print('Fetching grocery products from API...');
|
||||||
|
print(uri);
|
||||||
|
response = await http.get(uri).timeout(const Duration(seconds: 15));
|
||||||
|
} catch (_) {
|
||||||
|
throw Exception('Could not reach the server. Check your connection.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to load grocery products (HTTP ${response.statusCode})');
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Map<String, dynamic> body;
|
||||||
|
try {
|
||||||
|
body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
} catch (_) {
|
||||||
|
throw Exception('Received an unexpected response from the server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body['status'] != true) {
|
||||||
|
throw Exception(
|
||||||
|
(body['message'] as String?) ?? 'Failed to load grocery products');
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<dynamic> details =
|
||||||
|
(body['data'] as Map<String, dynamic>?)?['details'] as List<dynamic>? ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
return details.map((rawSection) {
|
||||||
|
final section = rawSection as Map<String, dynamic>;
|
||||||
|
final rawProducts = (section['products'] as List<dynamic>?) ?? [];
|
||||||
|
|
||||||
|
return GrocerySubcategorySection(
|
||||||
|
subcategoryId: (section['subcategoryid'] as num?)?.toInt() ?? 0,
|
||||||
|
subcategoryName: (section['subcategoryname'] as String?) ?? 'Other',
|
||||||
|
image: (section['image'] as String?) ?? 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKer1bKTGDnr5Um463cjhRTpKstbPxh5DxA2hrAr1egSWr7VK42aLkiI&s=10',
|
||||||
|
// Uses your existing Product.fromJson, so tenantid/productid/etc.
|
||||||
|
// needed by addToCart are preserved straight from the API.
|
||||||
|
products: Product.fromJsonList(rawProducts),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
526
lib/features/dashboard/presentation/screens/camera_search.dart
Normal file
526
lib/features/dashboard/presentation/screens/camera_search.dart
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../../../modules/search_model/search_model.dart';
|
||||||
|
import '../../../Products/product_card.dart';
|
||||||
|
import 'edit_object.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Full-screen camera-based visual product search, styled after
|
||||||
|
/// Google Lens / Amazon-style "search by image" camera views.
|
||||||
|
///
|
||||||
|
/// Capturing or picking a photo now hands off to [CropConfirmScreen],
|
||||||
|
/// which shows a draggable crop box (pre-filled from on-device object
|
||||||
|
/// detection) so the user can pick exactly which product to search for.
|
||||||
|
class LensSearchScreen extends StatefulWidget {
|
||||||
|
const LensSearchScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LensSearchScreen> createState() => _LensSearchScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LensSearchScreenState extends State<LensSearchScreen>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
CameraController? _controller;
|
||||||
|
List<CameraDescription> _cameras = [];
|
||||||
|
bool _flashOn = false;
|
||||||
|
bool _isInitializing = true;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
_setupCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupCamera() async {
|
||||||
|
try {
|
||||||
|
_cameras = await availableCameras();
|
||||||
|
if (_cameras.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'No camera found on this device.';
|
||||||
|
_isInitializing = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final backCamera = _cameras.firstWhere(
|
||||||
|
(cam) => cam.lensDirection == CameraLensDirection.back,
|
||||||
|
orElse: () => _cameras.first,
|
||||||
|
);
|
||||||
|
|
||||||
|
final controller = CameraController(
|
||||||
|
backCamera,
|
||||||
|
ResolutionPreset.high,
|
||||||
|
enableAudio: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.initialize();
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_controller = controller;
|
||||||
|
_isInitializing = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Could not start camera: $e';
|
||||||
|
_isInitializing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
final controller = _controller;
|
||||||
|
if (controller == null || !controller.value.isInitialized) return;
|
||||||
|
|
||||||
|
if (state == AppLifecycleState.inactive) {
|
||||||
|
controller.dispose();
|
||||||
|
} else if (state == AppLifecycleState.resumed) {
|
||||||
|
_setupCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_controller?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleFlash() async {
|
||||||
|
final controller = _controller;
|
||||||
|
if (controller == null) return;
|
||||||
|
final next = !_flashOn;
|
||||||
|
try {
|
||||||
|
await controller.setFlashMode(next ? FlashMode.torch : FlashMode.off);
|
||||||
|
setState(() => _flashOn = next);
|
||||||
|
} catch (_) {
|
||||||
|
// Some devices/emulators don't support torch mode; ignore silently.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _capturePhoto() async {
|
||||||
|
final controller = _controller;
|
||||||
|
if (controller == null || !controller.value.isInitialized) return;
|
||||||
|
if (controller.value.isTakingPicture) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final file = await controller.takePicture();
|
||||||
|
if (!mounted) return;
|
||||||
|
_goToCropConfirm(file.path);
|
||||||
|
} catch (e) {
|
||||||
|
_showSnack('Failed to capture photo: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFromGallery() async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final picked = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (picked == null) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
_goToCropConfirm(picked.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToCropConfirm(String imagePath) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => CropConfirmScreen(imagePath: imagePath),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSnack(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
_buildCameraPreview(),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 140,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [Colors.black54, Colors.transparent],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildTopBar(),
|
||||||
|
const Spacer(),
|
||||||
|
_buildHintPill(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildBottomBar(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCameraPreview() {
|
||||||
|
if (_isInitializing) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_error != null || _controller == null) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
_error ?? 'Camera unavailable',
|
||||||
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FittedBox(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _controller!.value.previewSize?.height ?? 1,
|
||||||
|
height: _controller!.value.previewSize?.width ?? 1,
|
||||||
|
child: CameraPreview(_controller!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTopBar() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: _LensWordmark()),
|
||||||
|
_CircleIconButton(
|
||||||
|
icon: _flashOn ? Icons.flash_on : Icons.flash_on_outlined,
|
||||||
|
onTap: _toggleFlash,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
_CircleIconButton(
|
||||||
|
icon: Icons.help_outline,
|
||||||
|
onTap: () =>
|
||||||
|
_showSnack('Point the camera at a product to search for it.'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHintPill() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.55),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Take a photo to search products',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomBar() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_CircleIconButton(
|
||||||
|
icon: Icons.add_photo_alternate_outlined,
|
||||||
|
onTap: _pickFromGallery,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
_ShutterButton(onTap: _capturePhoto),
|
||||||
|
_CircleIconButton(
|
||||||
|
icon: Icons.qr_code_scanner,
|
||||||
|
onTap: () => _showSnack('Barcode scan mode coming soon.'),
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LensWordmark extends StatelessWidget {
|
||||||
|
const _LensWordmark();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'Nearle',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'ai',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 2),
|
||||||
|
Icon(Icons.auto_awesome, color: Colors.white, size: 14),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CircleIconButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const _CircleIconButton({
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
this.size = 36,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.black.withOpacity(0.35),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: InkWell(
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
onTap: onTap,
|
||||||
|
child: SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Icon(icon, color: Colors.white, size: size * 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShutterButton extends StatelessWidget {
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ShutterButton({required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 76,
|
||||||
|
height: 76,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Colors.white54, width: 4),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.search, color: Colors.black87, size: 30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Results screen shown after crop confirmation.
|
||||||
|
///
|
||||||
|
/// [extractedText] is whatever on-device OCR (ML Kit Text Recognition)
|
||||||
|
/// found inside the user's cropped selection — e.g. a brand name like
|
||||||
|
/// "OPPO", model numbers, or other label text. It's shown in a dedicated
|
||||||
|
/// card with a "Copy" button so the user can grab it (paste into notes,
|
||||||
|
/// a search bar, a print dialog, wherever).
|
||||||
|
///
|
||||||
|
/// Replace `_fetchMatches` with a real call to your product-search API.
|
||||||
|
class SearchResultsScreen extends StatelessWidget {
|
||||||
|
final String imagePath;
|
||||||
|
final bool wasDetected;
|
||||||
|
final String extractedText;
|
||||||
|
|
||||||
|
const SearchResultsScreen({
|
||||||
|
super.key,
|
||||||
|
required this.imagePath,
|
||||||
|
this.wasDetected = false,
|
||||||
|
this.extractedText = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> _copyText(BuildContext context) async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: extractedText));
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Text copied to clipboard')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placeholder matches until this is wired to a real product-search API.
|
||||||
|
/// Swap this out for the actual API response mapped into [ProductItem]s.
|
||||||
|
/// NOTE: adjust field names here if they don't match your ProductItem
|
||||||
|
/// constructor exactly (image, category, name, rating, reviewCount,
|
||||||
|
/// price, originalPrice, isFavorite are assumed based on ProductCard).
|
||||||
|
List<ProductItem> _mockMatches() {
|
||||||
|
return List.generate(10, (index) {
|
||||||
|
final price = 15 + index * 7.5;
|
||||||
|
final original = price + 10;
|
||||||
|
return ProductItem(
|
||||||
|
image: 'https://picsum.photos/seed/product$index/300/300',
|
||||||
|
category: 'Electronics',
|
||||||
|
name: 'Matched product ${index + 1}',
|
||||||
|
rating: 4.5,
|
||||||
|
reviewCount: 20 + index,
|
||||||
|
price: '\$${price.toStringAsFixed(2)}',
|
||||||
|
originalPrice: '\$${original.toStringAsFixed(2)}',
|
||||||
|
isFavorite: false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasText = extractedText.trim().isNotEmpty;
|
||||||
|
final matches = _mockMatches();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
animateColor: false,
|
||||||
|
title: const Text('Search results')),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.file(
|
||||||
|
File(imagePath),
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
child: const Icon(Icons.image),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Showing matches for your cropped selection',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.black54),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (hasText) _buildDetectedTextCard(context),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
childAspectRatio: 0.72,
|
||||||
|
),
|
||||||
|
itemCount: matches.length,
|
||||||
|
itemBuilder: (context, index) => ProductCard(item: matches[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetectedTextCard(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.text_snippet_outlined,
|
||||||
|
size: 18, color: Colors.grey.shade700),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text(
|
||||||
|
'Detected text',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => _copyText(context),
|
||||||
|
icon: const Icon(Icons.copy, size: 16),
|
||||||
|
label: const Text('Copy'),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SelectableText(
|
||||||
|
extractedText,
|
||||||
|
style: const TextStyle(fontSize: 14, height: 1.4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1690
lib/features/dashboard/presentation/screens/daily_plan.dart
Normal file
1690
lib/features/dashboard/presentation/screens/daily_plan.dart
Normal file
File diff suppressed because it is too large
Load Diff
1551
lib/features/dashboard/presentation/screens/dash_view.dart
Normal file
1551
lib/features/dashboard/presentation/screens/dash_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
597
lib/features/dashboard/presentation/screens/edit_object.dart
Normal file
597
lib/features/dashboard/presentation/screens/edit_object.dart
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_mlkit_object_detection/google_mlkit_object_detection.dart';
|
||||||
|
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import '../../../../core/services/vector_service.dart';
|
||||||
|
import 'camera_search.dart';
|
||||||
|
|
||||||
|
/// Interactive crop-confirmation screen shown after a photo is captured
|
||||||
|
/// or picked. Mirrors the Google Lens / Amazon "search by image" pattern:
|
||||||
|
/// 1. The photo is shown with a draggable crop rectangle, pre-filled
|
||||||
|
/// with an ML Kit object-detection guess once it's ready.
|
||||||
|
/// 2. The user can drag the four corner handles (or the box itself) to
|
||||||
|
/// adjust which region gets searched.
|
||||||
|
/// 3. Tapping the confirm button crops the image, runs on-device OCR
|
||||||
|
/// on the cropped region to pull out any product text (brand,
|
||||||
|
/// model, etc.), sends both the image and text to the backend to
|
||||||
|
/// get vector embeddings, then shows an animated "Finding similar
|
||||||
|
/// products" sheet before navigating to the results screen.
|
||||||
|
class CropConfirmScreen extends StatefulWidget {
|
||||||
|
final String imagePath;
|
||||||
|
|
||||||
|
const CropConfirmScreen({super.key, required this.imagePath});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CropConfirmScreen> createState() => _CropConfirmScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _Corner { topLeft, topRight, bottomLeft, bottomRight }
|
||||||
|
|
||||||
|
class _CropConfirmScreenState extends State<CropConfirmScreen> {
|
||||||
|
final ObjectDetector _objectDetector = ObjectDetector(
|
||||||
|
options: ObjectDetectorOptions(
|
||||||
|
mode: DetectionMode.single,
|
||||||
|
classifyObjects: false,
|
||||||
|
multipleObjects: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final TextRecognizer _textRecognizer = TextRecognizer();
|
||||||
|
|
||||||
|
// Original decoded image dimensions (pixels).
|
||||||
|
double? _imgWidth;
|
||||||
|
double? _imgHeight;
|
||||||
|
|
||||||
|
// Crop rectangle in *original image pixel* coordinates.
|
||||||
|
Rect? _cropRect;
|
||||||
|
|
||||||
|
bool _isDetecting = true;
|
||||||
|
bool _isProcessing = false;
|
||||||
|
|
||||||
|
// Updated every build so gesture callbacks can convert screen <-> image
|
||||||
|
// coordinates using the current layout.
|
||||||
|
double _scale = 1;
|
||||||
|
double _offsetX = 0;
|
||||||
|
double _offsetY = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_objectDetector.close();
|
||||||
|
_textRecognizer.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
final bytes = await File(widget.imagePath).readAsBytes();
|
||||||
|
final decoded = img.decodeImage(bytes);
|
||||||
|
if (decoded == null || !mounted) return;
|
||||||
|
|
||||||
|
final w = decoded.width.toDouble();
|
||||||
|
final h = decoded.height.toDouble();
|
||||||
|
|
||||||
|
// Fallback: centered rect covering 80% of the image, used until (or
|
||||||
|
// unless) object detection finds something.
|
||||||
|
const inset = 0.1;
|
||||||
|
final fallback = Rect.fromLTRB(
|
||||||
|
w * inset,
|
||||||
|
h * inset,
|
||||||
|
w * (1 - inset),
|
||||||
|
h * (1 - inset),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_imgWidth = w;
|
||||||
|
_imgHeight = h;
|
||||||
|
_cropRect = fallback;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final inputImage = InputImage.fromFilePath(widget.imagePath);
|
||||||
|
final objects = await _objectDetector.processImage(inputImage);
|
||||||
|
if (!mounted || objects.isEmpty) {
|
||||||
|
setState(() => _isDetecting = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
objects.sort((a, b) {
|
||||||
|
final areaA = a.boundingBox.width * a.boundingBox.height;
|
||||||
|
final areaB = b.boundingBox.width * b.boundingBox.height;
|
||||||
|
return areaB.compareTo(areaA);
|
||||||
|
});
|
||||||
|
final box = objects.first.boundingBox;
|
||||||
|
setState(() {
|
||||||
|
_cropRect = Rect.fromLTRB(
|
||||||
|
box.left.clamp(0, w),
|
||||||
|
box.top.clamp(0, h),
|
||||||
|
box.right.clamp(0, w),
|
||||||
|
box.bottom.clamp(0, h),
|
||||||
|
);
|
||||||
|
_isDetecting = false;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() => _isDetecting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _minCropSize {
|
||||||
|
if (_imgWidth == null || _imgHeight == null) return 40;
|
||||||
|
return math.min(_imgWidth!, _imgHeight!) * 0.12;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateCorner(_Corner corner, Offset screenDelta) {
|
||||||
|
if (_cropRect == null) return;
|
||||||
|
final dx = screenDelta.dx / _scale;
|
||||||
|
final dy = screenDelta.dy / _scale;
|
||||||
|
final w = _imgWidth!;
|
||||||
|
final h = _imgHeight!;
|
||||||
|
final min = _minCropSize;
|
||||||
|
|
||||||
|
Rect r = _cropRect!;
|
||||||
|
switch (corner) {
|
||||||
|
case _Corner.topLeft:
|
||||||
|
final newLeft = (r.left + dx).clamp(0.0, r.right - min);
|
||||||
|
final newTop = (r.top + dy).clamp(0.0, r.bottom - min);
|
||||||
|
r = Rect.fromLTRB(newLeft, newTop, r.right, r.bottom);
|
||||||
|
break;
|
||||||
|
case _Corner.topRight:
|
||||||
|
final newRight = (r.right + dx).clamp(r.left + min, w);
|
||||||
|
final newTop = (r.top + dy).clamp(0.0, r.bottom - min);
|
||||||
|
r = Rect.fromLTRB(r.left, newTop, newRight, r.bottom);
|
||||||
|
break;
|
||||||
|
case _Corner.bottomLeft:
|
||||||
|
final newLeft = (r.left + dx).clamp(0.0, r.right - min);
|
||||||
|
final newBottom = (r.bottom + dy).clamp(r.top + min, h);
|
||||||
|
r = Rect.fromLTRB(newLeft, r.top, r.right, newBottom);
|
||||||
|
break;
|
||||||
|
case _Corner.bottomRight:
|
||||||
|
final newRight = (r.right + dx).clamp(r.left + min, w);
|
||||||
|
final newBottom = (r.bottom + dy).clamp(r.top + min, h);
|
||||||
|
r = Rect.fromLTRB(r.left, r.top, newRight, newBottom);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setState(() => _cropRect = r);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _moveRect(Offset screenDelta) {
|
||||||
|
if (_cropRect == null) return;
|
||||||
|
final dx = screenDelta.dx / _scale;
|
||||||
|
final dy = screenDelta.dy / _scale;
|
||||||
|
final w = _imgWidth!;
|
||||||
|
final h = _imgHeight!;
|
||||||
|
Rect r = _cropRect!.shift(Offset(dx, dy));
|
||||||
|
|
||||||
|
// Clamp so the rect stays fully inside the image bounds.
|
||||||
|
if (r.left < 0) r = r.shift(Offset(-r.left, 0));
|
||||||
|
if (r.top < 0) r = r.shift(Offset(0, -r.top));
|
||||||
|
if (r.right > w) r = r.shift(Offset(w - r.right, 0));
|
||||||
|
if (r.bottom > h) r = r.shift(Offset(0, h - r.bottom));
|
||||||
|
setState(() => _cropRect = r);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmCrop() async {
|
||||||
|
if (_cropRect == null || _isProcessing) return;
|
||||||
|
setState(() => _isProcessing = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final bytes = await File(widget.imagePath).readAsBytes();
|
||||||
|
final original = img.decodeImage(bytes);
|
||||||
|
if (original == null) throw Exception('Could not decode image');
|
||||||
|
|
||||||
|
final r = _cropRect!;
|
||||||
|
final x = r.left.toInt().clamp(0, original.width - 1);
|
||||||
|
final y = r.top.toInt().clamp(0, original.height - 1);
|
||||||
|
final cw = r.width.toInt().clamp(1, original.width - x);
|
||||||
|
final ch = r.height.toInt().clamp(1, original.height - y);
|
||||||
|
|
||||||
|
final cropped = img.copyCrop(original, x: x, y: y, width: cw, height: ch);
|
||||||
|
final croppedBytes = img.encodeJpg(cropped, quality: 92);
|
||||||
|
|
||||||
|
final dir = await getTemporaryDirectory();
|
||||||
|
final croppedPath =
|
||||||
|
'${dir.path}/cropped_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
|
await File(croppedPath).writeAsBytes(croppedBytes);
|
||||||
|
|
||||||
|
// Run on-device text recognition on the cropped region only, so we
|
||||||
|
// pick up whatever brand/model/label text is inside the user's box.
|
||||||
|
final extractedText = await _recognizeText(croppedPath);
|
||||||
|
|
||||||
|
// Send the cropped image + extracted text to the backend, which
|
||||||
|
// converts each into a vector embedding and returns both.
|
||||||
|
VectorEmbeddingResult? embeddings;
|
||||||
|
try {
|
||||||
|
embeddings = await VectorService.getEmbeddings(
|
||||||
|
imagePath: croppedPath,
|
||||||
|
labelText: extractedText,
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'Image vector (${embeddings.imageVector.length} dims): ${embeddings.imageVector}');
|
||||||
|
debugPrint(
|
||||||
|
'Text vector (${embeddings.textVector.length} dims): ${embeddings.textVector}');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Vector embedding failed: $e');
|
||||||
|
// decide whether to continue to results without vectors, or bail out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the "Finding similar products" sheet a moment to be visible
|
||||||
|
// before navigating — this is where a real search API call would
|
||||||
|
// happen instead of a fixed delay.
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1400));
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => SearchResultsScreen(
|
||||||
|
imagePath: croppedPath,
|
||||||
|
wasDetected: true,
|
||||||
|
extractedText: extractedText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isProcessing = false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Could not crop photo: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs ML Kit text recognition on [path] and returns the recognized
|
||||||
|
/// text (empty string if nothing was found or recognition failed).
|
||||||
|
Future<String> _recognizeText(String path) async {
|
||||||
|
try {
|
||||||
|
final inputImage = InputImage.fromFilePath(path);
|
||||||
|
final result = await _textRecognizer.processImage(inputImage);
|
||||||
|
return result.text.trim();
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFF15141B),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildTopBar(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Expanded(child: _buildImageArea()),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (!_isProcessing) _buildHintPill(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: _isProcessing
|
||||||
|
? null
|
||||||
|
: FloatingActionButton(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onPressed: _confirmCrop,
|
||||||
|
child: const Icon(Icons.check, color: Colors.black87),
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||||
|
bottomSheet: _isProcessing ? _buildProcessingSheet() : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTopBar() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Text(
|
||||||
|
'Nearle',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'ai',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 2),
|
||||||
|
Icon(Icons.auto_awesome, color: Colors.white, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const SizedBox(width: 48), // balances the back button
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHintPill() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_isDetecting ? 'Detecting product…' : 'Drag corners to adjust',
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageArea() {
|
||||||
|
if (_imgWidth == null || _imgHeight == null || _cropRect == null) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final availW = constraints.maxWidth;
|
||||||
|
final availH = constraints.maxHeight;
|
||||||
|
_scale = math.min(availW / _imgWidth!, availH / _imgHeight!);
|
||||||
|
final dispW = _imgWidth! * _scale;
|
||||||
|
final dispH = _imgHeight! * _scale;
|
||||||
|
_offsetX = (availW - dispW) / 2;
|
||||||
|
_offsetY = (availH - dispH) / 2;
|
||||||
|
|
||||||
|
final r = _cropRect!;
|
||||||
|
final screenRect = Rect.fromLTRB(
|
||||||
|
_offsetX + r.left * _scale,
|
||||||
|
_offsetY + r.top * _scale,
|
||||||
|
_offsetX + r.right * _scale,
|
||||||
|
_offsetY + r.bottom * _scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: _offsetX,
|
||||||
|
top: _offsetY,
|
||||||
|
width: dispW,
|
||||||
|
height: dispH,
|
||||||
|
child: Image.file(File(widget.imagePath), fit: BoxFit.fill),
|
||||||
|
),
|
||||||
|
..._buildDarkMask(screenRect, availW, availH),
|
||||||
|
Positioned.fromRect(
|
||||||
|
rect: screenRect,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onPanUpdate: (d) => _moveRect(d.delta),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.white, width: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
..._buildCornerHandles(screenRect),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildDarkMask(Rect r, double availW, double availH) {
|
||||||
|
const maskColor = Colors.black54;
|
||||||
|
return [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
height: r.top.clamp(0, availH),
|
||||||
|
child: IgnorePointer(child: Container(color: maskColor)),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: r.bottom,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: IgnorePointer(child: Container(color: maskColor)),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: r.top,
|
||||||
|
width: r.left.clamp(0, availW),
|
||||||
|
height: r.height,
|
||||||
|
child: IgnorePointer(child: Container(color: maskColor)),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: r.right,
|
||||||
|
top: r.top,
|
||||||
|
right: 0,
|
||||||
|
height: r.height,
|
||||||
|
child: IgnorePointer(child: Container(color: maskColor)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildCornerHandles(Rect r) {
|
||||||
|
return [
|
||||||
|
_cornerHandle(r.topLeft, _Corner.topLeft),
|
||||||
|
_cornerHandle(r.topRight, _Corner.topRight),
|
||||||
|
_cornerHandle(r.bottomLeft, _Corner.bottomLeft),
|
||||||
|
_cornerHandle(r.bottomRight, _Corner.bottomRight),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _cornerHandle(Offset position, _Corner corner) {
|
||||||
|
const hitSize = 44.0;
|
||||||
|
return Positioned(
|
||||||
|
left: position.dx - hitSize / 2,
|
||||||
|
top: position.dy - hitSize / 2,
|
||||||
|
width: hitSize,
|
||||||
|
height: hitSize,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onPanUpdate: (d) => _updateCorner(corner, d.delta),
|
||||||
|
child: Center(
|
||||||
|
child: CustomPaint(
|
||||||
|
size: const Size(28, 28),
|
||||||
|
painter: _CornerBracketPainter(corner: corner),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProcessingSheet() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(28),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 55,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
child: Lottie.asset(
|
||||||
|
'assets/lotties/ai_loader.json',
|
||||||
|
height: 140,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
const Text(
|
||||||
|
"Searching Similar Products",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Analyzing image, detecting text\nand finding the closest matches...",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: const LinearProgressIndicator(
|
||||||
|
minHeight: 8,
|
||||||
|
backgroundColor: Color(0xffECECEC),
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"This usually takes a few seconds",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a white L-shaped bracket oriented for the given corner, matching
|
||||||
|
/// the Google Lens style crop handles.
|
||||||
|
class _CornerBracketPainter extends CustomPainter {
|
||||||
|
final _Corner corner;
|
||||||
|
|
||||||
|
_CornerBracketPainter({required this.corner});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..strokeWidth = 3
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final path = Path();
|
||||||
|
switch (corner) {
|
||||||
|
case _Corner.topLeft:
|
||||||
|
path.moveTo(0, size.height);
|
||||||
|
path.lineTo(0, 0);
|
||||||
|
path.lineTo(size.width, 0);
|
||||||
|
break;
|
||||||
|
case _Corner.topRight:
|
||||||
|
path.moveTo(0, 0);
|
||||||
|
path.lineTo(size.width, 0);
|
||||||
|
path.lineTo(size.width, size.height);
|
||||||
|
break;
|
||||||
|
case _Corner.bottomLeft:
|
||||||
|
path.moveTo(0, 0);
|
||||||
|
path.lineTo(0, size.height);
|
||||||
|
path.lineTo(size.width, size.height);
|
||||||
|
break;
|
||||||
|
case _Corner.bottomRight:
|
||||||
|
path.moveTo(size.width, 0);
|
||||||
|
path.lineTo(size.width, size.height);
|
||||||
|
path.lineTo(0, size.height);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _CornerBracketPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||||
|
|
||||||
|
// Adjust these import paths to match your project structure.
|
||||||
|
import '../../../../constants/color_constants.dart';
|
||||||
|
import '../../../../widgets/text_widget.dart';
|
||||||
|
import 'ai_processing_screen.dart';
|
||||||
|
|
||||||
|
// TODO: replace with your actual search/results screen import + widget.
|
||||||
|
// import '../search/search_result_screen.dart';
|
||||||
|
|
||||||
|
class VoiceAssistantScreen extends StatefulWidget {
|
||||||
|
const VoiceAssistantScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VoiceAssistantScreen> createState() => _VoiceAssistantScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VoiceAssistantScreenState extends State<VoiceAssistantScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
final stt.SpeechToText _speech = stt.SpeechToText();
|
||||||
|
|
||||||
|
bool _speechAvailable = false;
|
||||||
|
bool isListening = true;
|
||||||
|
String recognizedText = "Tap the mic and start speaking";
|
||||||
|
|
||||||
|
late final AnimationController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_initSpeech();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
_speech.stop();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- SPEECH SETUP ----------------
|
||||||
|
|
||||||
|
Future<void> _initSpeech() async {
|
||||||
|
_speechAvailable = await _speech.initialize(
|
||||||
|
onStatus: (status) {
|
||||||
|
// Fires when recognition stops on its own (silence / done)
|
||||||
|
if (status == 'notListening' || status == 'done') {
|
||||||
|
if (isListening) {
|
||||||
|
setState(() => isListening = false);
|
||||||
|
_handleFinalResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
setState(() => isListening = false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleListening() async {
|
||||||
|
if (!_speechAvailable) return;
|
||||||
|
|
||||||
|
if (isListening) {
|
||||||
|
await _speech.stop();
|
||||||
|
setState(() => isListening = false);
|
||||||
|
_handleFinalResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isListening = true;
|
||||||
|
recognizedText = "Listening...";
|
||||||
|
});
|
||||||
|
|
||||||
|
await _speech.listen(
|
||||||
|
onResult: (result) {
|
||||||
|
setState(() {
|
||||||
|
recognizedText = result.recognizedWords;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called once listening stops (either the user tapped stop, or the
|
||||||
|
/// recognizer auto-stopped after silence). Navigates forward with
|
||||||
|
/// whatever was captured, as long as it isn't empty.
|
||||||
|
void _handleFinalResult() {
|
||||||
|
final query = recognizedText.trim();
|
||||||
|
|
||||||
|
final isPlaceholder = query.isEmpty ||
|
||||||
|
query == "Listening..." ||
|
||||||
|
query == "Tap the mic and start speaking";
|
||||||
|
|
||||||
|
if (isPlaceholder) return;
|
||||||
|
|
||||||
|
// TODO: swap this for your real navigation, e.g.:
|
||||||
|
// Navigator.pushReplacement(
|
||||||
|
// context,
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (_) => SearchResultScreen(query: query),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// If this screen was opened for a result (e.g. from a search bar),
|
||||||
|
// you can instead just pop with the value:
|
||||||
|
// Navigator.pop(context, query);
|
||||||
|
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => AIAssistantScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: ColorConstants.secondaryColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_buildHeader(context),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_buildStatusChip(),
|
||||||
|
const SizedBox(height: 26),
|
||||||
|
_buildMicStage(),
|
||||||
|
const SizedBox(height: 26),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: ReusableTextWidget(
|
||||||
|
text: recognizedText,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
color: ColorConstants.blackColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildPremiumMicButton(),
|
||||||
|
const SizedBox(height: 22),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- HEADER ----------------
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(18, 14, 18, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Navigator.maybePop(context),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorConstants.lightGreyBg,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.arrow_back_ios_new_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: ColorConstants.darkGreyColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: ReusableTextWidget(
|
||||||
|
text: "Voice Assistant",
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: ColorConstants.blackColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusChip() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorConstants.primaryColor1,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 8,
|
||||||
|
width: 8,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: ColorConstants.primaryColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: isListening ? "Listening" : "Paused",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: ColorConstants.primaryColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- MIC / LOTTIE ----------------
|
||||||
|
|
||||||
|
Widget _buildMicStage() {
|
||||||
|
return SizedBox(
|
||||||
|
height: 270,
|
||||||
|
width: 270,
|
||||||
|
child: Center(
|
||||||
|
child: Lottie.asset(
|
||||||
|
"assets/lotties/ai.json",
|
||||||
|
height: 250,
|
||||||
|
width: 250,
|
||||||
|
repeat: true,
|
||||||
|
animate: isListening,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- PREMIUM MIC BUTTON ----------------
|
||||||
|
|
||||||
|
Widget _buildPremiumMicButton() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _toggleListening,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final ringOpacity =
|
||||||
|
isListening ? 0.18 + (controller.value * 0.18) : 0.0;
|
||||||
|
return Container(
|
||||||
|
height: 92,
|
||||||
|
width: 92,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color:
|
||||||
|
ColorConstants.primaryColor1.withOpacity(ringOpacity),
|
||||||
|
),
|
||||||
|
child: Center(child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 68,
|
||||||
|
width: 68,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [ColorConstants.primaryColor, Color(0xFF8C3EAE)],
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorConstants.primaryColor.withOpacity(0.4),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isListening ? Icons.mic_rounded : Icons.mic_off_rounded,
|
||||||
|
color: ColorConstants.secondaryColor,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: !_speechAvailable
|
||||||
|
? "Speech recognition unavailable"
|
||||||
|
: isListening
|
||||||
|
? "Tap to stop listening"
|
||||||
|
: "Tap to speak",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: ColorConstants.lightGrey,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporary placeholder so the file compiles and navigation is visible.
|
||||||
|
/// Delete this and point `_handleFinalResult` at your real screen.
|
||||||
|
class _PlaceholderResultScreen extends StatelessWidget {
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
const _PlaceholderResultScreen({required this.query});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("Search Results")),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
'You said: "$query"\n\nReplace _PlaceholderResultScreen with your real search/results page.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
744
lib/features/orders/presentation/order_tracking.dart
Normal file
744
lib/features/orders/presentation/order_tracking.dart
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
// delivery_tracking_page.dart
|
||||||
|
//
|
||||||
|
// SETUP:
|
||||||
|
// 1. pubspec.yaml -> add:
|
||||||
|
// google_maps_flutter: ^2.9.0
|
||||||
|
// url_launcher: ^6.3.0 (for calling the delivery partner)
|
||||||
|
//
|
||||||
|
// 2. Android -> android/app/src/main/AndroidManifest.xml, inside <application>:
|
||||||
|
// <meta-data
|
||||||
|
// android:name="com.google.android.geo.API_KEY"
|
||||||
|
// android:value="xxx" />
|
||||||
|
//
|
||||||
|
// 3. iOS -> ios/Runner/AppDelegate.swift, inside application(didFinishLaunchingWithOptions):
|
||||||
|
// GMSServices.provideAPIKey("xxx")
|
||||||
|
//
|
||||||
|
// Replace "xxx" above with your real Google Maps API key in BOTH places.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../../widgets/text_widget.dart';
|
||||||
|
|
||||||
|
/// Draws a small circular badge marker at runtime (no external icon assets
|
||||||
|
/// needed) — a colored circle with an icon inside, similar to Swiggy's
|
||||||
|
/// bike / restaurant / home pins.
|
||||||
|
Future<BitmapDescriptor> _drawBadgeMarker({
|
||||||
|
required IconData icon,
|
||||||
|
required Color background,
|
||||||
|
Color iconColor = Colors.white,
|
||||||
|
double size = 60,
|
||||||
|
}) async {
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
final canvas = Canvas(recorder);
|
||||||
|
final paint = Paint()..color = background;
|
||||||
|
final radius = size / 2;
|
||||||
|
|
||||||
|
canvas.drawCircle(Offset(radius, radius), radius, Paint()..color = Colors.white);
|
||||||
|
canvas.drawCircle(Offset(radius, radius), radius - 6, paint);
|
||||||
|
|
||||||
|
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||||
|
textPainter.text = TextSpan(
|
||||||
|
text: String.fromCharCode(icon.codePoint),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size * 0.5,
|
||||||
|
fontFamily: icon.fontFamily,
|
||||||
|
package: icon.fontPackage,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
textPainter.layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(radius - textPainter.width / 2, radius - textPainter.height / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
final picture = recorder.endRecording();
|
||||||
|
final img = await picture.toImage(size.toInt(), size.toInt());
|
||||||
|
final bytes = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Your Google Maps API key (also enable it for "Directions API").
|
||||||
|
const String kGoogleApiKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q";
|
||||||
|
|
||||||
|
/// Custom map style JSON — a clean, low-saturation grey theme.
|
||||||
|
const String kSwiggyLikeMapStyle = '''
|
||||||
|
[
|
||||||
|
{"elementType": "geometry", "stylers": [{"color": "#f5f5f3"}]},
|
||||||
|
{"elementType": "labels.icon", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"elementType": "labels.text.fill", "stylers": [{"color": "#8a8a8a"}]},
|
||||||
|
{"elementType": "labels.text.stroke", "stylers": [{"color": "#f5f5f3"}]},
|
||||||
|
{"featureType": "administrative", "elementType": "geometry", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "administrative.land_parcel", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "administrative.neighborhood", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "poi", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "poi.park", "elementType": "geometry", "stylers": [{"color": "#e5e8e0"}]},
|
||||||
|
{"featureType": "road", "elementType": "geometry", "stylers": [{"color": "#ffffff"}]},
|
||||||
|
{"featureType": "road", "elementType": "geometry.stroke", "stylers": [{"color": "#e3e3e3"}]},
|
||||||
|
{"featureType": "road.arterial", "elementType": "labels", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "road.highway", "elementType": "geometry", "stylers": [{"color": "#f2c9a0"}]},
|
||||||
|
{"featureType": "road.local", "elementType": "labels", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "transit", "stylers": [{"visibility": "off"}]},
|
||||||
|
{"featureType": "water", "elementType": "geometry", "stylers": [{"color": "#c9d6df"}]}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
|
||||||
|
class DeliveryTrackingPage extends StatefulWidget {
|
||||||
|
const DeliveryTrackingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DeliveryTrackingPage> createState() => _DeliveryTrackingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeliveryTrackingPageState extends State<DeliveryTrackingPage> {
|
||||||
|
GoogleMapController? _mapController;
|
||||||
|
|
||||||
|
String _partnerName = 'Majid Khan';
|
||||||
|
double _partnerRating = 4.8;
|
||||||
|
int _partnerOrders = 1240;
|
||||||
|
|
||||||
|
// Sample coordinates - replace with your real pickup / drop / rider coords
|
||||||
|
final LatLng _pickup = const LatLng(28.6450, 77.3550); // restaurant
|
||||||
|
final LatLng _drop = const LatLng(28.6395, 77.3480); // customer
|
||||||
|
LatLng _riderPosition = const LatLng(28.6440, 77.3520);
|
||||||
|
|
||||||
|
final Set<Marker> _markers = {};
|
||||||
|
final Set<Polyline> _polylines = {};
|
||||||
|
List<LatLng> _routePoints = [];
|
||||||
|
|
||||||
|
Timer? _riderTimer;
|
||||||
|
int _routeIndex = 0;
|
||||||
|
int _etaMinutes = 2;
|
||||||
|
|
||||||
|
BitmapDescriptor? _pickupIcon;
|
||||||
|
BitmapDescriptor? _dropIcon;
|
||||||
|
BitmapDescriptor? _riderIcon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadIcons() async {
|
||||||
|
_pickupIcon = await _drawBadgeMarker(
|
||||||
|
icon: Icons.storefront,
|
||||||
|
background: const Color(0xFFFF6A00),
|
||||||
|
);
|
||||||
|
_dropIcon = await _drawBadgeMarker(
|
||||||
|
icon: Icons.home,
|
||||||
|
background: const Color(0xFF7A1F5C),
|
||||||
|
);
|
||||||
|
_riderIcon = await _drawBadgeMarker(
|
||||||
|
icon: Icons.two_wheeler,
|
||||||
|
background: const Color(0xFF662582),
|
||||||
|
size: 60,
|
||||||
|
);
|
||||||
|
_setupMarkers();
|
||||||
|
_fetchRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_riderTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupMarkers() {
|
||||||
|
_markers
|
||||||
|
..clear()
|
||||||
|
..addAll([
|
||||||
|
Marker(
|
||||||
|
markerId: const MarkerId('pickup'),
|
||||||
|
position: _pickup,
|
||||||
|
icon: _pickupIcon ??
|
||||||
|
BitmapDescriptor.defaultMarkerWithHue(
|
||||||
|
BitmapDescriptor.hueOrange),
|
||||||
|
infoWindow: const InfoWindow(title: 'Wrapperz'),
|
||||||
|
),
|
||||||
|
Marker(
|
||||||
|
markerId: const MarkerId('drop'),
|
||||||
|
position: _drop,
|
||||||
|
icon: _dropIcon ??
|
||||||
|
BitmapDescriptor.defaultMarkerWithHue(
|
||||||
|
BitmapDescriptor.hueViolet),
|
||||||
|
infoWindow: const InfoWindow(title: 'Friends and Family'),
|
||||||
|
),
|
||||||
|
Marker(
|
||||||
|
markerId: const MarkerId('rider'),
|
||||||
|
position: _riderPosition,
|
||||||
|
icon: _riderIcon ??
|
||||||
|
BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
|
||||||
|
infoWindow: const InfoWindow(title: 'MAJID - delivery partner'),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a real road-following route from Google Directions API and
|
||||||
|
/// draws it as a polyline.
|
||||||
|
Future<void> _fetchRoute() async {
|
||||||
|
try {
|
||||||
|
final url = Uri.parse(
|
||||||
|
'https://maps.googleapis.com/maps/api/directions/json'
|
||||||
|
'?origin=${_pickup.latitude},${_pickup.longitude}'
|
||||||
|
'&destination=${_drop.latitude},${_drop.longitude}'
|
||||||
|
'&mode=driving'
|
||||||
|
'&key=$kGoogleApiKey',
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await http.get(url);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
if (data['status'] == 'OK') {
|
||||||
|
final points = data['routes'][0]['overview_polyline']['points'];
|
||||||
|
_routePoints = _decodePolyline(points);
|
||||||
|
} else {
|
||||||
|
_routePoints = _fallbackRoute();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_routePoints = _fallbackRoute();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
_routePoints = _fallbackRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_polylines
|
||||||
|
..clear()
|
||||||
|
..add(
|
||||||
|
Polyline(
|
||||||
|
polylineId: const PolylineId('route'),
|
||||||
|
points: _routePoints,
|
||||||
|
color: const Color(0xFF662582),
|
||||||
|
width: 5,
|
||||||
|
startCap: Cap.roundCap,
|
||||||
|
endCap: Cap.roundCap,
|
||||||
|
jointType: JointType.round,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
_startRiderAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple zig-zag fallback so the UI still shows a route without a live key.
|
||||||
|
List<LatLng> _fallbackRoute() {
|
||||||
|
return [
|
||||||
|
_pickup,
|
||||||
|
LatLng(_pickup.latitude - 0.0025, _pickup.longitude - 0.0015),
|
||||||
|
LatLng(_pickup.latitude - 0.0035, _pickup.longitude - 0.0060),
|
||||||
|
LatLng(_pickup.latitude - 0.0060, _pickup.longitude - 0.0075),
|
||||||
|
_drop,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LatLng> _decodePolyline(String encoded) {
|
||||||
|
List<LatLng> points = [];
|
||||||
|
int index = 0, len = encoded.length;
|
||||||
|
int lat = 0, lng = 0;
|
||||||
|
|
||||||
|
while (index < len) {
|
||||||
|
int b, shift = 0, result = 0;
|
||||||
|
do {
|
||||||
|
b = encoded.codeUnitAt(index++) - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (b >= 0x20);
|
||||||
|
int dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||||
|
lat += dlat;
|
||||||
|
|
||||||
|
shift = 0;
|
||||||
|
result = 0;
|
||||||
|
do {
|
||||||
|
b = encoded.codeUnitAt(index++) - 63;
|
||||||
|
result |= (b & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (b >= 0x20);
|
||||||
|
int dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||||
|
lng += dlng;
|
||||||
|
|
||||||
|
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the rider marker along the route to simulate live tracking.
|
||||||
|
void _startRiderAnimation() {
|
||||||
|
if (_routePoints.isEmpty) return;
|
||||||
|
_riderTimer?.cancel();
|
||||||
|
_riderTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
|
||||||
|
if (_routeIndex >= _routePoints.length - 1) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_routeIndex++;
|
||||||
|
_riderPosition = _routePoints[_routeIndex];
|
||||||
|
_setupMarkers();
|
||||||
|
setState(() {});
|
||||||
|
_mapController?.animateCamera(CameraUpdate.newLatLng(_riderPosition));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _callPartner() async {
|
||||||
|
final uri = Uri(scheme: 'tel', path: '+910000000000');
|
||||||
|
if (await canLaunchUrl(uri)) await launchUrl(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: SizedBox(),
|
||||||
|
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'Out for delivery',
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '11:38 AM • 1 items',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: const Color(0xFF662582),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// ---------------- MAP ----------------
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.6,
|
||||||
|
child: GoogleMap(
|
||||||
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: _pickup,
|
||||||
|
zoom: 14.5,
|
||||||
|
),
|
||||||
|
markers: _markers,
|
||||||
|
polylines: _polylines,
|
||||||
|
myLocationButtonEnabled: false,
|
||||||
|
zoomControlsEnabled: false,
|
||||||
|
compassEnabled: false,
|
||||||
|
mapToolbarEnabled: false,
|
||||||
|
buildingsEnabled: false,
|
||||||
|
indoorViewEnabled: false,
|
||||||
|
trafficEnabled: false,
|
||||||
|
style: kSwiggyLikeMapStyle,
|
||||||
|
onMapCreated: (controller) => _mapController = controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// -------------- TOP BAR --------------
|
||||||
|
|
||||||
|
|
||||||
|
// SafeArea(
|
||||||
|
// child: Padding(
|
||||||
|
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
// child: Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// children: [
|
||||||
|
// _circleIconButton(Icons.arrow_back, () {
|
||||||
|
// Navigator.maybePop(context);
|
||||||
|
// }),
|
||||||
|
// Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
// children: [
|
||||||
|
// ReusableTextWidget(
|
||||||
|
// text: 'Out for delivery',
|
||||||
|
// fontSize: 17,
|
||||||
|
// fontWeight: FontWeight.w700,
|
||||||
|
// color: Colors.grey.shade900,
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 2),
|
||||||
|
// ReusableTextWidget(
|
||||||
|
// text: '11:38 AM • 1 items',
|
||||||
|
// fontSize: 12,
|
||||||
|
// fontWeight: FontWeight.w500,
|
||||||
|
// color: Colors.grey.shade600,
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// const SizedBox(width: 40),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// -------------- BOTTOM SHEET --------------
|
||||||
|
DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.42,
|
||||||
|
minChildSize: 0.42,
|
||||||
|
maxChildSize: 0.9,
|
||||||
|
builder: (context, scrollController) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ListView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
// Out for delivery row
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'Out for delivery',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'MAJID is on the way to deliver your order',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF7A1F5C),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '$_etaMinutes',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'mins',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Delivery partner info card
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
backgroundImage: const NetworkImage(
|
||||||
|
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSmbomkQ5Aqtd848F_UVR86eY1wrq5tuNJRjmRXAGgXsqJtlL5H6EF8Efws&s=10'),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade500,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: _partnerName,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.star_rounded,
|
||||||
|
size: 16, color: Colors.amber.shade700),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '$_partnerRating',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '• $_partnerOrders orders',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: _callPartner,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: const Color(0xFFFCE4EC),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.call,
|
||||||
|
color: Colors.redAccent, size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Action buttons row
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Container(height: 8, color: const Color(0xFFF5F5F5)),
|
||||||
|
|
||||||
|
// Product / order items card
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.receipt_long, size: 20, color: Colors.grey.shade700),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'Order ID: # 123456789',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Items list
|
||||||
|
_buildBillItem('Chicken Biryani', 1, 250),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildBillItem('Mutton Biryani', 1, 350),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Price breakdown
|
||||||
|
_buildPriceRow('Subtotal', 600),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildPriceRow('Delivery Fee', 40),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildPriceRow('Tax (5%)', 30),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Total
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'Total',
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.grey.shade900,
|
||||||
|
),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '₹670',
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.grey.shade900,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButton({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required Color color,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: color),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: label,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBillItem(String name, int qty, double price) {
|
||||||
|
final total = qty * price;
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ReusableTextWidget(
|
||||||
|
text: '${qty}x $name',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '₹${price.toStringAsFixed(0)}',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '₹${total.toStringAsFixed(0)}',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPriceRow(String label, double amount) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: label,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: '₹${amount.toStringAsFixed(0)}',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _circleIconButton(IconData icon, VoidCallback onTap) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black26, blurRadius: 6),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: Colors.black87, size: 20),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
390
lib/main.dart
390
lib/main.dart
@@ -1,8 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:alp_animated_splashscreen/alp_animated_splashscreen.dart';
|
import 'package:alp_animated_splashscreen/alp_animated_splashscreen.dart';
|
||||||
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -10,11 +9,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:in_app_update/in_app_update.dart';
|
||||||
import 'package:nearledaily/constants/color_constants.dart';
|
import 'package:nearledaily/constants/color_constants.dart';
|
||||||
import 'package:nearledaily/view/authentication/app_update_view.dart';
|
import 'package:nearledaily/view/authentication/app_update_view.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||||
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:new_version_plus/new_version_plus.dart';
|
import 'package:new_version_plus/new_version_plus.dart';
|
||||||
|
|
||||||
@@ -26,35 +27,27 @@ import 'controllers/dashboard_controller/dashboard_controller.dart';
|
|||||||
import 'controllers/intro_controller/intro_screen_controller.dart';
|
import 'controllers/intro_controller/intro_screen_controller.dart';
|
||||||
import 'controllers/order_controller/create_order_controller.dart';
|
import 'controllers/order_controller/create_order_controller.dart';
|
||||||
import 'controllers/tenant/get_tenant.dart';
|
import 'controllers/tenant/get_tenant.dart';
|
||||||
import 'controllers/tenant_controller /tenant_list.dart';
|
import 'controllers/tenant_list/tenant_controller.dart';
|
||||||
import 'helper/firebase_options.dart';
|
|
||||||
|
import 'core/services/ExitWrapper.dart';
|
||||||
|
import 'features/dashboard/presentation/screens/ai_processing_screen.dart';
|
||||||
import 'service/bindings.dart';
|
import 'service/bindings.dart';
|
||||||
import 'service/device_info/device_info.dart';
|
import 'service/device_info/device_info.dart';
|
||||||
import 'view/home_view.dart';
|
import 'view/home_view.dart';
|
||||||
import 'view/splash_view/splash_view.dart';
|
import 'view/splash_view/splash_view.dart';
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// FIREBASE BACKGROUND HANDLER
|
|
||||||
// -------------------------
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp();
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
);
|
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
|
||||||
FlutterLocalNotificationsPlugin();
|
|
||||||
|
|
||||||
const AndroidInitializationSettings androidSettings =
|
const AndroidInitializationSettings androidSettings =
|
||||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin.initialize(
|
await flutterLocalNotificationsPlugin.initialize(
|
||||||
const InitializationSettings(android: androidSettings),
|
const InitializationSettings(android: androidSettings),
|
||||||
);
|
);
|
||||||
|
|
||||||
final androidPlugin = flutterLocalNotificationsPlugin
|
final androidPlugin = flutterLocalNotificationsPlugin
|
||||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>();
|
||||||
await androidPlugin?.createNotificationChannel(
|
await androidPlugin?.createNotificationChannel(
|
||||||
const AndroidNotificationChannel(
|
const AndroidNotificationChannel(
|
||||||
'nearle_channel',
|
'nearle_channel',
|
||||||
@@ -67,81 +60,56 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|||||||
final title = data['title'] as String?;
|
final title = data['title'] as String?;
|
||||||
final body = data['body'] as String?;
|
final body = data['body'] as String?;
|
||||||
|
|
||||||
if (title != null && title.isNotEmpty) {
|
if (title == null || title.isEmpty) return;
|
||||||
await flutterLocalNotificationsPlugin.show(
|
|
||||||
999,
|
await flutterLocalNotificationsPlugin.show(
|
||||||
title,
|
999,
|
||||||
body?.isNotEmpty == true ? body : null,
|
title,
|
||||||
NotificationDetails(
|
body?.isNotEmpty == true ? body : null,
|
||||||
android: AndroidNotificationDetails(
|
NotificationDetails(
|
||||||
'nearle_channel',
|
android: AndroidNotificationDetails(
|
||||||
'Nearle Notifications',
|
'nearle_channel',
|
||||||
importance: Importance.max,
|
'Nearle Notifications',
|
||||||
priority: Priority.high,
|
importance: Importance.max,
|
||||||
fullScreenIntent: true,
|
priority: Priority.high,
|
||||||
playSound: true,
|
fullScreenIntent: true,
|
||||||
enableVibration: true,
|
playSound: true,
|
||||||
largeIcon: const DrawableResourceAndroidBitmap('nearle_logo.jpeg'),
|
enableVibration: true,
|
||||||
),
|
largeIcon: const DrawableResourceAndroidBitmap('nearle_logo.jpeg'),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// LOCAL NOTIFICATIONS SETUP
|
|
||||||
// -------------------------
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
Future<void> _setupLocalNotifications() async {
|
Future<void> _setupLocalNotifications() async {
|
||||||
// Android settings
|
|
||||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||||
|
const InitializationSettings initializationSettings =
|
||||||
// iOS/macOS settings
|
InitializationSettings(android: initializationSettingsAndroid);
|
||||||
const DarwinInitializationSettings initializationSettingsIOS =
|
|
||||||
DarwinInitializationSettings(
|
|
||||||
requestSoundPermission: true,
|
|
||||||
requestBadgePermission: true,
|
|
||||||
requestAlertPermission: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Combine all platforms
|
|
||||||
const InitializationSettings initializationSettings = InitializationSettings(
|
|
||||||
android: initializationSettingsAndroid,
|
|
||||||
iOS: initializationSettingsIOS,
|
|
||||||
macOS: initializationSettingsIOS,
|
|
||||||
);
|
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||||
|
|
||||||
// Android-only notification channel
|
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
'nearle_channel',
|
||||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
'Nearle Notifications',
|
||||||
'nearle_channel',
|
description: 'High priority notifications',
|
||||||
'Nearle Notifications',
|
importance: Importance.max,
|
||||||
description: 'High priority notifications',
|
playSound: true,
|
||||||
importance: Importance.max,
|
enableVibration: true,
|
||||||
playSound: true,
|
);
|
||||||
enableVibration: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin
|
await flutterLocalNotificationsPlugin
|
||||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
.resolvePlatformSpecificImplementation<
|
||||||
?.createNotificationChannel(channel);
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
}
|
?.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// ROUTE OBSERVER
|
|
||||||
// -------------------------
|
|
||||||
final RouteObserver<ModalRoute<void>> routeObserver =
|
final RouteObserver<ModalRoute<void>> routeObserver =
|
||||||
RouteObserver<ModalRoute<void>>();
|
RouteObserver<ModalRoute<void>>();
|
||||||
|
|
||||||
// -------------------------
|
// PRINT CURRENT + STORE VERSION (unchanged)
|
||||||
// APP VERSION CHECK
|
|
||||||
// -------------------------
|
|
||||||
Future<void> _printAndCheckAppVersions() async {
|
Future<void> _printAndCheckAppVersions() async {
|
||||||
try {
|
try {
|
||||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
@@ -159,7 +127,10 @@ Future<void> _printAndCheckAppVersions() async {
|
|||||||
await prefs.setString('currentAppVersion', currentVersion);
|
await prefs.setString('currentAppVersion', currentVersion);
|
||||||
|
|
||||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||||
final newVersion = NewVersionPlus(androidId: "com.nearle.gear");
|
final newVersion = NewVersionPlus(
|
||||||
|
androidId: "com.nearle.gear",
|
||||||
|
);
|
||||||
|
|
||||||
final status = await newVersion.getVersionStatus();
|
final status = await newVersion.getVersionStatus();
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
print("PLAY STORE VERSION: ${status.storeVersion}");
|
print("PLAY STORE VERSION: ${status.storeVersion}");
|
||||||
@@ -179,7 +150,7 @@ Future<void> _printAndCheckAppVersions() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if update available
|
// FIXED: Now correctly detects real update from Play Store
|
||||||
Future<bool> _checkForAppUpdate() async {
|
Future<bool> _checkForAppUpdate() async {
|
||||||
if (defaultTargetPlatform != TargetPlatform.android) return false;
|
if (defaultTargetPlatform != TargetPlatform.android) return false;
|
||||||
|
|
||||||
@@ -190,24 +161,18 @@ Future<bool> _checkForAppUpdate() async {
|
|||||||
if (status != null && status.canUpdate) {
|
if (status != null && status.canUpdate) {
|
||||||
print("UPDATE DETECTED! Redirecting to AppUpdateView");
|
print("UPDATE DETECTED! Redirecting to AppUpdateView");
|
||||||
print("Current: ${status.localVersion} → Store: ${status.storeVersion}");
|
print("Current: ${status.localVersion} → Store: ${status.storeVersion}");
|
||||||
return true;
|
return true; // This will now go to AppUpdateView
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Version check failed: $e");
|
print("Version check failed (continuing anyway): $e");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// HANDLE NOTIFICATION TAP
|
|
||||||
// -------------------------
|
|
||||||
void _handleMessage(RemoteMessage message) {
|
void _handleMessage(RemoteMessage message) {
|
||||||
print("Notification tapped: ${message.data}");
|
print("Notification tapped: ${message.data}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// ANIMATED SPLASH
|
|
||||||
// -------------------------
|
|
||||||
class AnimatedSplashWithNavigation extends StatefulWidget {
|
class AnimatedSplashWithNavigation extends StatefulWidget {
|
||||||
final Widget nextScreen;
|
final Widget nextScreen;
|
||||||
const AnimatedSplashWithNavigation({super.key, required this.nextScreen});
|
const AnimatedSplashWithNavigation({super.key, required this.nextScreen});
|
||||||
@@ -237,162 +202,166 @@ class _AnimatedSplashWithNavigationState
|
|||||||
backgroundcolor: Colors.white,
|
backgroundcolor: Colors.white,
|
||||||
foregroundcolor: ColorConstants.primaryColor,
|
foregroundcolor: ColorConstants.primaryColor,
|
||||||
brandnamecolor: ColorConstants.primaryColor,
|
brandnamecolor: ColorConstants.primaryColor,
|
||||||
|
// companyname: 'Nearle Daily',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// MAIN ENTRY POINT
|
|
||||||
// -------------------------
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Firebase initialize with options (iOS/web safe)
|
FlutterError.onError = (details) => FlutterError.dumpErrorToConsole(details);
|
||||||
await Firebase.initializeApp(
|
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _printAndCheckAppVersions();
|
await runZonedGuarded(() async {
|
||||||
await _setupLocalNotifications();
|
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
SystemUiOverlayStyle(statusBarColor: Colors.grey[200]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
await Firebase.initializeApp();
|
||||||
prefs.setBool("firstTime", true);
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// NOTIFICATION PERMISSIONS & FCM TOKEN
|
|
||||||
// -------------------------
|
|
||||||
if (!kIsWeb) {
|
|
||||||
if (Platform.isIOS) {
|
await _printAndCheckAppVersions();
|
||||||
NotificationSettings settings =
|
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
SystemUiOverlayStyle(statusBarColor: Colors.grey[200]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _setupLocalNotifications();
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
|
RemoteMessage? initialMessage =
|
||||||
|
await FirebaseMessaging.instance.getInitialMessage();
|
||||||
|
if (initialMessage != null) _handleMessage(initialMessage);
|
||||||
|
|
||||||
|
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
|
||||||
|
|
||||||
|
if (!kIsWeb) {
|
||||||
await FirebaseMessaging.instance.requestPermission(
|
await FirebaseMessaging.instance.requestPermission(
|
||||||
alert: true,
|
alert: true,
|
||||||
badge: true,
|
badge: true,
|
||||||
sound: true,
|
sound: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
|
|
||||||
print('User granted permission');
|
|
||||||
|
|
||||||
// Listen for token refresh (APNs ready)
|
|
||||||
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
|
|
||||||
print('FCM Token (iOS APNs ready): $newToken');
|
|
||||||
await prefs.setString('fcmToken', newToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optional: small delay initial attempt
|
|
||||||
Future.delayed(const Duration(seconds: 2), () async {
|
|
||||||
String? token = await FirebaseMessaging.instance.getToken();
|
|
||||||
print('FCM Token initial attempt (iOS): $token');
|
|
||||||
if (token != null) await prefs.setString('fcmToken', token);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
print('User declined notification permission');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Android: get token immediately
|
|
||||||
String? token = await FirebaseMessaging.instance.getToken();
|
|
||||||
print('FCM Token (Android): $token');
|
|
||||||
if (token != null) await prefs.setString('fcmToken', token);
|
|
||||||
}
|
}
|
||||||
}
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setBool("firstTime", true);
|
||||||
|
|
||||||
// Foreground notifications
|
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
final data = message.data;
|
||||||
final data = message.data;
|
final title = data['title'] ?? message.notification?.title ?? 'Nearle';
|
||||||
final title = data['title'] ?? message.notification?.title ?? 'Nearle';
|
final body = data['body'] ?? message.notification?.body ?? '';
|
||||||
final body = data['body'] ?? message.notification?.body ?? '';
|
|
||||||
|
|
||||||
final ByteData jpegData =
|
final ByteData jpegData =
|
||||||
await rootBundle.load('assets/images/nearledaily.png');
|
await rootBundle.load('assets/images/nearledaily.png');
|
||||||
final Uint8List jpegBytes = jpegData.buffer.asUint8List();
|
final Uint8List jpegBytes = jpegData.buffer.asUint8List();
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
await flutterLocalNotificationsPlugin.show(
|
||||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
NotificationDetails(
|
NotificationDetails(
|
||||||
android: AndroidNotificationDetails(
|
android: AndroidNotificationDetails(
|
||||||
'nearle_channel',
|
'nearle_channel',
|
||||||
'Nearle Notifications',
|
'Nearle Notifications',
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.high,
|
priority: Priority.high,
|
||||||
fullScreenIntent: true,
|
fullScreenIntent: true,
|
||||||
playSound: true,
|
playSound: true,
|
||||||
enableVibration: true,
|
enableVibration: true,
|
||||||
largeIcon: ByteArrayAndroidBitmap(jpegBytes),
|
largeIcon: ByteArrayAndroidBitmap(jpegBytes),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
iOS: const DarwinNotificationDetails(),
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
String? fcmToken = await FirebaseMessaging.instance.getToken();
|
||||||
|
await prefs.setString('fcmToken', fcmToken ?? '');
|
||||||
|
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
final params = PlatformWebViewControllerCreationParams();
|
||||||
|
AndroidWebViewController(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiConstants.tenantCustomers = ApiConstants.tenantCustomerLive;
|
||||||
|
ApiConstants.orderedtenantCustomers = ApiConstants.orderedtenantCustomerLive;
|
||||||
|
ApiConstants.login = ApiConstants.loginLive;
|
||||||
|
|
||||||
|
Get.put(TenantController(), permanent: true);
|
||||||
|
Get.lazyPut(() => AuthController(), fenix: true);
|
||||||
|
Get.lazyPut(() => DashboardController(), fenix: true);
|
||||||
|
Get.lazyPut(() => CartController(), fenix: true);
|
||||||
|
Get.lazyPut(() => BottomNavController(), fenix: true);
|
||||||
|
Get.lazyPut(() => OrderedTenantController(), fenix: true);
|
||||||
|
Get.lazyPut(() => OrderController(), fenix: true);
|
||||||
|
Get.lazyPut(() => FaqController(), fenix: true);
|
||||||
|
Get.lazyPut(() => IntroScreenController(), fenix: true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DeviceInfo deviceInfo = DeviceInfo();
|
||||||
|
await deviceInfo.getDeviceInfo();
|
||||||
|
|
||||||
|
await SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final int? customerId = prefs.getInt('customerId');
|
||||||
|
final String? contactNo = prefs.getString('contactno');
|
||||||
|
bool updateAvailable = await _checkForAppUpdate(); // Now works correctly!
|
||||||
|
|
||||||
|
Widget nextScreen;
|
||||||
|
if (updateAvailable) {
|
||||||
|
nextScreen = const AppUpdateView();
|
||||||
|
} else if (customerId != null && contactNo != null && contactNo.isNotEmpty) {
|
||||||
|
nextScreen = BottomNavigation();
|
||||||
|
} else {
|
||||||
|
nextScreen = const SplashScreenView();
|
||||||
|
}
|
||||||
|
// --- Deep link check, overrides nextScreen if a valid link exists ---
|
||||||
|
try {
|
||||||
|
final initialUri = await AppLinks().getInitialLink();
|
||||||
|
debugPrint('DEEPLINK initial: $initialUri');
|
||||||
|
if (initialUri != null &&
|
||||||
|
initialUri.scheme == 'nearle' &&
|
||||||
|
initialUri.host == 'order') {
|
||||||
|
final item = initialUri.queryParameters['item'];
|
||||||
|
final budget = initialUri.queryParameters['budget'];
|
||||||
|
nextScreen = AIAssistantScreen();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('DEEPLINK initial error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.white, // or transparent
|
||||||
|
statusBarIconBrightness: Brightness.dark, // Android
|
||||||
|
statusBarBrightness: Brightness.light, // iOS
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// Android WebView Controller
|
runApp(MyApp(startScreen: nextScreen));
|
||||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
AppLinks().uriLinkStream.listen((uri) {
|
||||||
final params = PlatformWebViewControllerCreationParams();
|
debugPrint('DEEPLINK stream: $uri');
|
||||||
AndroidWebViewController(params);
|
if (uri.scheme == 'nearle' && uri.host == 'order') {
|
||||||
}
|
final item = uri.queryParameters['item'];
|
||||||
|
final budget = uri.queryParameters['budget'];
|
||||||
// API constants
|
Get.to(() => AIAssistantScreen());
|
||||||
ApiConstants.tenantCustomers = ApiConstants.tenantCustomerLive;
|
}
|
||||||
ApiConstants.orderedtenantCustomers = ApiConstants.orderedtenantCustomerLive;
|
});
|
||||||
ApiConstants.login = ApiConstants.loginLive;
|
}, (error, stack) => print('Error: $error'));
|
||||||
|
|
||||||
// Controllers
|
|
||||||
Get.put(TenantController(), permanent: true);
|
|
||||||
Get.lazyPut(() => AuthController(), fenix: true);
|
|
||||||
Get.lazyPut(() => DashboardController(), fenix: true);
|
|
||||||
Get.lazyPut(() => CartController(), fenix: true);
|
|
||||||
Get.lazyPut(() => BottomNavController(), fenix: true);
|
|
||||||
Get.lazyPut(() => OrderedTenantController(), fenix: true);
|
|
||||||
Get.lazyPut(() => OrderController(), fenix: true);
|
|
||||||
Get.lazyPut(() => FaqController(), fenix: true);
|
|
||||||
Get.lazyPut(() => IntroScreenController(), fenix: true);
|
|
||||||
|
|
||||||
DeviceInfo deviceInfo = DeviceInfo();
|
|
||||||
await deviceInfo.getDeviceInfo();
|
|
||||||
|
|
||||||
await SystemChrome.setPreferredOrientations([
|
|
||||||
DeviceOrientation.portraitUp,
|
|
||||||
DeviceOrientation.portraitDown,
|
|
||||||
]);
|
|
||||||
|
|
||||||
final int? customerId = prefs.getInt('customerId');
|
|
||||||
final String? contactNo = prefs.getString('contactno');
|
|
||||||
bool updateAvailable = await _checkForAppUpdate();
|
|
||||||
|
|
||||||
Widget nextScreen;
|
|
||||||
if (updateAvailable) {
|
|
||||||
nextScreen = const AppUpdateView();
|
|
||||||
} else if (customerId != null && contactNo != null && contactNo.isNotEmpty) {
|
|
||||||
nextScreen = BottomNavigation();
|
|
||||||
} else {
|
|
||||||
nextScreen = const SplashScreenView();
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
const SystemUiOverlayStyle(
|
|
||||||
statusBarColor: Colors.white,
|
|
||||||
statusBarIconBrightness: Brightness.dark,
|
|
||||||
statusBarBrightness: Brightness.light,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
runApp(MyApp(startScreen: nextScreen));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// MAIN APP
|
|
||||||
// -------------------------
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Widget startScreen;
|
final Widget startScreen;
|
||||||
const MyApp({super.key, required this.startScreen});
|
const MyApp({super.key, required this.startScreen});
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetMaterialApp(
|
return GetMaterialApp(
|
||||||
@@ -404,6 +373,7 @@ class MyApp extends StatelessWidget {
|
|||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||||
),
|
),
|
||||||
initialBinding: GlobalBinding(),
|
initialBinding: GlobalBinding(),
|
||||||
|
|
||||||
home: AnimatedSplashWithNavigation(nextScreen: startScreen),
|
home: AnimatedSplashWithNavigation(nextScreen: startScreen),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
24
lib/modules/search/grocery_item.dart
Normal file
24
lib/modules/search/grocery_item.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class GroceryItem {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String quantity;
|
||||||
|
final String emoji;
|
||||||
|
bool inCart;
|
||||||
|
|
||||||
|
GroceryItem({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.quantity,
|
||||||
|
required this.emoji,
|
||||||
|
this.inCart = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GroceryItem.fromJson(int id, Map<String, dynamic> json) {
|
||||||
|
return GroceryItem(
|
||||||
|
id: id,
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
quantity: json['quantity'] ?? '',
|
||||||
|
emoji: json['emoji'] ?? '🛒',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/modules/search_model/search_model.dart
Normal file
69
lib/modules/search_model/search_model.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// ─── Data Model ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
class GroceryItem {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String quantity;
|
||||||
|
final String emoji;
|
||||||
|
bool inCart;
|
||||||
|
|
||||||
|
GroceryItem({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.quantity,
|
||||||
|
required this.emoji,
|
||||||
|
this.inCart = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GroceryItem.fromJson(int id, Map<String, dynamic> json) {
|
||||||
|
return GroceryItem(
|
||||||
|
id: id,
|
||||||
|
name: json['name']?.toString() ?? '',
|
||||||
|
quantity: json['quantity']?.toString() ?? '',
|
||||||
|
emoji: json['emoji']?.toString() ?? '🛒',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecommendedItem {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final String image;
|
||||||
|
final Color bgColor;
|
||||||
|
final Color accentColor;
|
||||||
|
|
||||||
|
RecommendedItem({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.image,
|
||||||
|
required this.bgColor,
|
||||||
|
required this.accentColor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProductItem {
|
||||||
|
final String name;
|
||||||
|
final String category;
|
||||||
|
final String price;
|
||||||
|
final String originalPrice;
|
||||||
|
final String image;
|
||||||
|
final double rating;
|
||||||
|
final int reviewCount;
|
||||||
|
final bool isFavorite;
|
||||||
|
|
||||||
|
ProductItem({
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.price,
|
||||||
|
required this.originalPrice,
|
||||||
|
required this.image,
|
||||||
|
required this.rating,
|
||||||
|
required this.reviewCount,
|
||||||
|
this.isFavorite = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
58
lib/service/ai_service/api_service.dart
Normal file
58
lib/service/ai_service/api_service.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
class GroqService {
|
||||||
|
static const String _apiKey = 'YOUR_API_KEY';
|
||||||
|
|
||||||
|
static Future<String> fetchIngredients(String query) async {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse('https://api.groq.com/openai/v1/chat/completions'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $_apiKey',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'model': 'llama-3.1-8b-instant',
|
||||||
|
'max_tokens': 600,
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content':
|
||||||
|
'Return ONLY JSON array of grocery items...'
|
||||||
|
},
|
||||||
|
{'role': 'user', 'content': query}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = jsonDecode(res.body);
|
||||||
|
return data['choices'][0]['message']['content'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AiSearchService {
|
||||||
|
static const String baseUrl =
|
||||||
|
'http://10.0.2.2:8000/find-ingredients';
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> search(String query) async {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse(baseUrl),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
"user_input": query,
|
||||||
|
"customer_id": 6060,
|
||||||
|
"latitude": 11.0050728,
|
||||||
|
"longitude": 76.9508513
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return jsonDecode(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception("Failed to load results");
|
||||||
|
}
|
||||||
|
}
|
||||||
0
lib/service/ai_service/product_service.dart
Normal file
0
lib/service/ai_service/product_service.dart
Normal file
0
lib/service/ai_service/store_service.dart
Normal file
0
lib/service/ai_service/store_service.dart
Normal file
28
lib/service/firebase_analytics/analytics_service.dart
Normal file
28
lib/service/firebase_analytics/analytics_service.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
|
||||||
|
class AnalyticsService {
|
||||||
|
static final FirebaseAnalytics analytics =
|
||||||
|
FirebaseAnalytics.instance;
|
||||||
|
|
||||||
|
static Future<void> logScreenView(String screenName) async {
|
||||||
|
await analytics.logScreenView(
|
||||||
|
screenName: screenName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> logLogin(String method) async {
|
||||||
|
await analytics.logLogin(
|
||||||
|
loginMethod: method,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> logEvent(
|
||||||
|
String name, {
|
||||||
|
Map<String, Object>? parameters,
|
||||||
|
}) async {
|
||||||
|
await analytics.logEvent(
|
||||||
|
name: name,
|
||||||
|
parameters: parameters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -277,17 +277,8 @@ class _AccountPageState extends State<AccountPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_divider(),
|
|
||||||
_tile(
|
|
||||||
icon: Icons.question_answer,
|
|
||||||
title: "Faq",
|
|
||||||
onTap: () => Get.to(
|
|
||||||
() => FaqView(),
|
|
||||||
// transition: Transition.fade, // or any transition you like
|
|
||||||
// duration: Duration(milliseconds: 400),
|
|
||||||
),
|
|
||||||
|
|
||||||
),
|
|
||||||
_divider(),
|
_divider(),
|
||||||
_tile(
|
_tile(
|
||||||
icon: Icons.reorder,
|
icon: Icons.reorder,
|
||||||
@@ -318,15 +309,15 @@ class _AccountPageState extends State<AccountPage> {
|
|||||||
onTap: controller.rateApp,
|
onTap: controller.rateApp,
|
||||||
),
|
),
|
||||||
_divider(),
|
_divider(),
|
||||||
// _tile(
|
_tile(
|
||||||
// icon: Icons.group_add,
|
icon: Icons.group_add,
|
||||||
// title: "Refer a Friend",
|
title: "Refer a Friend",
|
||||||
// onTap: () => Get.to(
|
onTap: () => Get.to(
|
||||||
// () => const ShowContactsScreen(),
|
() => ReferEarnScreen(),
|
||||||
// // transition: Transition.fade, // or any style you like
|
// transition: Transition.fade, // or any style you like
|
||||||
// // duration: Duration(milliseconds: 400),
|
// duration: Duration(milliseconds: 400),
|
||||||
// ),
|
),
|
||||||
// ),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -347,6 +338,17 @@ class _AccountPageState extends State<AccountPage> {
|
|||||||
// duration: Duration(milliseconds: 400),
|
// duration: Duration(milliseconds: 400),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
),
|
||||||
|
_divider(),
|
||||||
|
_tile(
|
||||||
|
icon: Icons.question_answer,
|
||||||
|
title: "Faq",
|
||||||
|
onTap: () => Get.to(
|
||||||
|
() => FaqView(),
|
||||||
|
// transition: Transition.fade, // or any transition you like
|
||||||
|
// duration: Duration(milliseconds: 400),
|
||||||
|
),
|
||||||
|
|
||||||
),
|
),
|
||||||
_divider(),
|
_divider(),
|
||||||
_tile(
|
_tile(
|
||||||
|
|||||||
@@ -1,379 +1,396 @@
|
|||||||
// import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
// import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter/services.dart';
|
||||||
// import 'package:nearledaily/constants/color_constants.dart';
|
|
||||||
// import 'package:permission_handler/permission_handler.dart'
|
import '../../constants/color_constants.dart';
|
||||||
// as permission_handler;
|
import '../../constants/font_constants.dart';
|
||||||
// import 'package:url_launcher/url_launcher.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
// import 'package:flutter/services.dart';
|
|
||||||
//
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// import '../../constants/font_constants.dart';
|
// ReferEarnScreen (StatefulWidget)
|
||||||
// import '../../widgets/text_widget.dart';
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
//
|
|
||||||
// class ShowContactsScreen extends StatefulWidget {
|
class ReferEarnScreen extends StatefulWidget {
|
||||||
// const ShowContactsScreen({super.key});
|
const ReferEarnScreen({super.key});
|
||||||
//
|
|
||||||
// @override
|
@override
|
||||||
// State<ShowContactsScreen> createState() => _ShowContactsScreenState();
|
State<ReferEarnScreen> createState() => _ReferEarnScreenState();
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// class _ShowContactsScreenState extends State<ShowContactsScreen>
|
class _ReferEarnScreenState extends State<ReferEarnScreen> {
|
||||||
// with WidgetsBindingObserver {
|
bool _codeCopied = false;
|
||||||
// List<Contact> _contacts = [];
|
|
||||||
// bool _loading = false;
|
static const String _referralCode = 'ANBU123';
|
||||||
// bool _permissionDenied = false;
|
static const int _totalCoins = 1250;
|
||||||
//
|
|
||||||
// /// 🔹 ADDED
|
|
||||||
// bool _showDisclaimer = true;
|
// ── helpers ────────────────────────────────────────────────────────────────
|
||||||
//
|
void _copyCode() async {
|
||||||
// @override
|
await Clipboard.setData(const ClipboardData(text: _referralCode));
|
||||||
// void initState() {
|
setState(() => _codeCopied = true);
|
||||||
// super.initState();
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
// WidgetsBinding.instance.addObserver(this);
|
if (mounted) setState(() => _codeCopied = false);
|
||||||
// _loadContacts();
|
}
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @override
|
// ── build ──────────────────────────────────────────────────────────────────
|
||||||
// void dispose() {
|
@override
|
||||||
// WidgetsBinding.instance.removeObserver(this);
|
Widget build(BuildContext context) {
|
||||||
// super.dispose();
|
return SafeArea(
|
||||||
// }
|
top: false,
|
||||||
//
|
bottom: true,
|
||||||
// Future<void> _loadContacts() async {
|
child: Scaffold(
|
||||||
// setState(() {
|
backgroundColor: Color(0xFFF6F6F6),
|
||||||
// _loading = true;
|
appBar: AppBar(
|
||||||
// _permissionDenied = false;
|
|
||||||
// });
|
surfaceTintColor: Colors.transparent,
|
||||||
//
|
|
||||||
// final bool granted = await FlutterContacts.requestPermission();
|
// 🔥 Prevent color overlay when scrolled
|
||||||
//
|
scrolledUnderElevation: 0,
|
||||||
// if (!granted) {
|
animateColor: false, // ✨ prevent color change on scroll
|
||||||
// setState(() {
|
elevation: 0,
|
||||||
// _loading = false;
|
backgroundColor: Colors.white,
|
||||||
// _permissionDenied = true;
|
|
||||||
// });
|
leading: const BackButton(color: Color(0xFF1A1A2E)),
|
||||||
// return;
|
|
||||||
// }
|
title: ReusableTextWidget(
|
||||||
//
|
text: 'Refer & Earn',
|
||||||
// try {
|
color: const Color(0xFF1A1A2E),
|
||||||
// final List<Contact> contacts = await FlutterContacts.getContacts(
|
fontFamily: FontConstants.fontFamily,
|
||||||
// withProperties: true,
|
fontSize: 18,
|
||||||
// withPhoto: true,
|
fontWeight: FontWeight.w700,
|
||||||
// );
|
),
|
||||||
//
|
),
|
||||||
// setState(() {
|
body: SingleChildScrollView(
|
||||||
// _contacts = contacts
|
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 16),
|
||||||
// .where((c) => c.phones.isNotEmpty)
|
child: Column(
|
||||||
// .toList()
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// ..sort((a, b) => a.displayName.compareTo(b.displayName));
|
children: [
|
||||||
// _loading = false;
|
_buildTotalCoinsBanner(),
|
||||||
// });
|
const SizedBox(height: 16),
|
||||||
// } catch (e) {
|
_buildReferralCodeCard(),
|
||||||
// setState(() {
|
const SizedBox(height: 20),
|
||||||
// _loading = false;
|
|
||||||
// });
|
Center(
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
child: ReusableTextWidget(
|
||||||
// SnackBar(
|
text: 'How It Works',
|
||||||
// content: ReusableTextWidget(
|
color: const Color(0xFF1A1A2E),
|
||||||
// text: "Error loading contacts: $e",
|
fontFamily: FontConstants.fontFamily,
|
||||||
// fontSize: 14,
|
fontSize: 16,
|
||||||
// fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w700,
|
||||||
// fontFamily: FontConstants.fontFamily,
|
),
|
||||||
// color: Colors.white,
|
),
|
||||||
// ),
|
SizedBox(height: 16),
|
||||||
// ),
|
_buildHowItWorksRow(),
|
||||||
// );
|
const SizedBox(height: 24),
|
||||||
// }
|
],
|
||||||
// }
|
),
|
||||||
//
|
),
|
||||||
// Widget _buildAvatar(Contact contact) {
|
),
|
||||||
// if (contact.photo != null && contact.photo!.isNotEmpty) {
|
);
|
||||||
// return CircleAvatar(
|
}
|
||||||
// backgroundImage: MemoryImage(contact.photo!),
|
|
||||||
// );
|
// ── Total Coins Banner ─────────────────────────────────────────────────────
|
||||||
// } else {
|
|
||||||
// String initials = "";
|
Widget _buildTotalCoinsBanner() {
|
||||||
// final names = contact.displayName.split(" ");
|
|
||||||
// if (names.isNotEmpty) initials += names[0][0];
|
|
||||||
// if (names.length > 1) initials += names[1][0];
|
return Container(
|
||||||
// return CircleAvatar(
|
decoration: BoxDecoration(
|
||||||
// backgroundColor: Colors.primaries[
|
borderRadius: BorderRadius.circular(20),
|
||||||
// contact.displayName.hashCode % Colors.primaries.length],
|
gradient: LinearGradient(
|
||||||
// child: ReusableTextWidget(
|
colors: [
|
||||||
// text: initials.toUpperCase(),
|
ColorConstants.primaryColor.withOpacity(0.85),
|
||||||
// fontSize: 16,
|
ColorConstants.primaryColor,
|
||||||
// fontWeight: FontWeight.bold,
|
],
|
||||||
// fontFamily: FontConstants.fontFamily,
|
begin: Alignment.topLeft,
|
||||||
// color: Colors.white,
|
end: Alignment.bottomRight,
|
||||||
// ),
|
),
|
||||||
// );
|
),
|
||||||
// }
|
padding: const EdgeInsets.all(20),
|
||||||
// }
|
child: Column(
|
||||||
//
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// Future<void> _openWhatsApp(Contact contact) async {
|
children: [
|
||||||
// if (contact.phones.isEmpty) return;
|
Row(
|
||||||
//
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// String phoneNumber =
|
children: [
|
||||||
// contact.phones.first.number.replaceAll(RegExp(r'\D'), '');
|
Expanded(
|
||||||
// final Uri url = Uri.parse("https://wa.me/$phoneNumber");
|
child: Column(
|
||||||
//
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// if (await canLaunchUrl(url)) {
|
children: [
|
||||||
// await launchUrl(url, mode: LaunchMode.externalApplication);
|
ReusableTextWidget(
|
||||||
// } else {
|
text: 'Total Coins',
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
color: Colors.white,
|
||||||
// const SnackBar(
|
fontFamily: FontConstants.fontFamily,
|
||||||
// content: ReusableTextWidget(
|
fontSize: 13,
|
||||||
// text: "Could not open WhatsApp",
|
fontWeight: FontWeight.w800,
|
||||||
// fontSize: 14,
|
),
|
||||||
// fontWeight: FontWeight.w400,
|
const SizedBox(height: 4),
|
||||||
// fontFamily: FontConstants.fontFamily,
|
ReusableTextWidget(
|
||||||
// color: Colors.white,
|
text: '$_totalCoins Coins',
|
||||||
// ),
|
color: Colors.white,
|
||||||
// ),
|
fontFamily: FontConstants.fontFamily,
|
||||||
// );
|
fontSize: 28,
|
||||||
// }
|
fontWeight: FontWeight.w800,
|
||||||
// }
|
),
|
||||||
//
|
const SizedBox(height: 4),
|
||||||
// Future<void> _inviteWhatsApp(Contact contact) async {
|
ReusableTextWidget(
|
||||||
// if (contact.phones.isEmpty) return;
|
text: 'Earn more by referring friends',
|
||||||
//
|
color: Colors.white60,
|
||||||
// String phoneNumber =
|
fontFamily: FontConstants.fontFamily,
|
||||||
// contact.phones.first.number.replaceAll(RegExp(r'\D'), '');
|
fontSize: 12,
|
||||||
//
|
fontWeight: FontWeight.w400,
|
||||||
// final String message = Uri.encodeComponent(
|
),
|
||||||
// "Hey! Join me on Nearle Daily 🚀");
|
],
|
||||||
//
|
),
|
||||||
// final Uri url = Uri.parse("https://wa.me/$phoneNumber?text=$message");
|
),
|
||||||
//
|
Container(
|
||||||
// if (await canLaunchUrl(url)) {
|
width: 80,
|
||||||
// await launchUrl(url, mode: LaunchMode.externalApplication);
|
height: 70,
|
||||||
// } else {
|
decoration: BoxDecoration(
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
borderRadius: BorderRadius.circular(12),
|
||||||
// const SnackBar(
|
color: Colors.white.withOpacity(0.15),
|
||||||
// content: ReusableTextWidget(
|
),
|
||||||
// text: "Could not open WhatsApp",
|
child: const Center(
|
||||||
// fontSize: 14,
|
child: Text('🪙', style: TextStyle(fontSize: 36)),
|
||||||
// fontWeight: FontWeight.w400,
|
),
|
||||||
// fontFamily: FontConstants.fontFamily,
|
),
|
||||||
// color: Colors.white,
|
],
|
||||||
// ),
|
),
|
||||||
// ),
|
const SizedBox(height: 16),
|
||||||
// );
|
],
|
||||||
// }
|
),
|
||||||
// }
|
);
|
||||||
//
|
}
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
// ── Referral Code Card ─────────────────────────────────────────────────────
|
||||||
// return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
||||||
// value: const SystemUiOverlayStyle(
|
Widget _buildReferralCodeCard() {
|
||||||
// statusBarColor: Colors.white, // White background
|
return Container(
|
||||||
// statusBarIconBrightness: Brightness.dark, // Dark icons
|
decoration: BoxDecoration(
|
||||||
// statusBarBrightness: Brightness.light, // iOS
|
color: Colors.white,
|
||||||
// ),
|
borderRadius: BorderRadius.circular(16),
|
||||||
// child: Scaffold(
|
|
||||||
// backgroundColor: Colors.white,
|
),
|
||||||
// appBar: AppBar(
|
padding: const EdgeInsets.all(16),
|
||||||
// backgroundColor: Colors.white,
|
child: Column(
|
||||||
// surfaceTintColor: Colors.transparent,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// scrolledUnderElevation: 0,
|
children: [
|
||||||
// titleSpacing: -5,
|
ReusableTextWidget(
|
||||||
// animateColor: false,
|
text: 'Your Referral Code',
|
||||||
// elevation: 0,
|
color: Colors.black.withOpacity(0.7),
|
||||||
// title: ReusableTextWidget(
|
fontFamily: FontConstants.fontFamily,
|
||||||
// text: "Refer a friend",
|
fontSize: 14,
|
||||||
// fontSize: 20,
|
fontWeight: FontWeight.w600,
|
||||||
// fontWeight: FontWeight.w600,
|
),
|
||||||
// fontFamily: FontConstants.fontFamily,
|
const SizedBox(height: 12),
|
||||||
// color: Colors.black,
|
Container(
|
||||||
// ),
|
decoration: BoxDecoration(
|
||||||
// iconTheme: const IconThemeData(color: Colors.black),
|
border: Border.all(
|
||||||
// ),
|
color: Colors.black.withOpacity(0.7),
|
||||||
// body: Padding(
|
width: 1.5),
|
||||||
// padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 12),
|
borderRadius: BorderRadius.circular(10),
|
||||||
// child: Column(
|
),
|
||||||
// children: [
|
padding:
|
||||||
// /// 🔹 MODIFIED DISCLAIMER ONLY
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
// if (_showDisclaimer)
|
child: Row(
|
||||||
// Stack(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
// children: [
|
children: [
|
||||||
// Padding(
|
ReusableTextWidget(
|
||||||
// padding: const EdgeInsets.only(top: 12.0),
|
text: _referralCode,
|
||||||
// child: Container(
|
color: Colors.black.withOpacity(0.7),
|
||||||
// width: double.infinity,
|
fontFamily: FontConstants.fontFamily,
|
||||||
// padding: const EdgeInsets.all(14),
|
fontSize: 20,
|
||||||
// margin: const EdgeInsets.only(bottom: 16),
|
fontWeight: FontWeight.w700,
|
||||||
// decoration: BoxDecoration(
|
),
|
||||||
// color: ColorConstants.primaryColor.withOpacity(0.08),
|
GestureDetector(
|
||||||
// borderRadius: BorderRadius.circular(12),
|
onTap: _copyCode,
|
||||||
// ),
|
child: AnimatedSwitcher(
|
||||||
// child: const ReusableTextWidget(
|
duration: const Duration(milliseconds: 200),
|
||||||
// text:
|
child: _codeCopied
|
||||||
// "We access contacts only to let you share\nor recommend to friends. Nothing is stored.",
|
? Row(
|
||||||
// fontSize: 13,
|
key: const ValueKey('copied'),
|
||||||
// fontWeight: FontWeight.w500,
|
children: [
|
||||||
// fontFamily: FontConstants.fontFamily,
|
const Icon(Icons.check_circle,
|
||||||
// color: Colors.black87,
|
size: 18, color: Color(0xFF2ECC71)),
|
||||||
// ),
|
const SizedBox(width: 4),
|
||||||
// ),
|
ReusableTextWidget(
|
||||||
// ),
|
text: 'Copied!',
|
||||||
// Positioned(
|
color: const Color(0xFF2ECC71),
|
||||||
// top: 6,
|
fontFamily: FontConstants.fontFamily,
|
||||||
// right: -3,
|
fontSize: 14,
|
||||||
// child: IconButton(
|
fontWeight: FontWeight.w600,
|
||||||
// icon: const Icon(Icons.close, size: 18),
|
),
|
||||||
// onPressed: () {
|
],
|
||||||
// setState(() {
|
)
|
||||||
// _showDisclaimer = false;
|
: Row(
|
||||||
// });
|
key: const ValueKey('copy'),
|
||||||
// },
|
children: [
|
||||||
// ),
|
Icon(Icons.copy,
|
||||||
// ),
|
size: 18,
|
||||||
// ],
|
color: ColorConstants.primaryColor),
|
||||||
// ),
|
const SizedBox(width: 4),
|
||||||
//
|
ReusableTextWidget(
|
||||||
// if (_loading)
|
text: 'Copy',
|
||||||
// const Expanded(
|
color: ColorConstants.primaryColor,
|
||||||
// child: Center(child: CircularProgressIndicator()),
|
fontFamily: FontConstants.fontFamily,
|
||||||
// ),
|
fontSize: 14,
|
||||||
//
|
fontWeight: FontWeight.w600,
|
||||||
// if (_permissionDenied)
|
),
|
||||||
// Expanded(
|
],
|
||||||
// child: Center(
|
),
|
||||||
// child: Container(
|
),
|
||||||
// margin: const EdgeInsets.symmetric(horizontal: 24),
|
),
|
||||||
// padding: const EdgeInsets.all(24),
|
],
|
||||||
// decoration: BoxDecoration(
|
),
|
||||||
// color: Colors.red.withOpacity(0.05),
|
),
|
||||||
// borderRadius: BorderRadius.circular(20),
|
const SizedBox(height: 14),
|
||||||
// border: Border.all(
|
SizedBox(
|
||||||
// color: Colors.red.withOpacity(0.2),
|
width: double.infinity,
|
||||||
// ),
|
child: ElevatedButton.icon(
|
||||||
// ),
|
onPressed: () {},
|
||||||
// child: Column(
|
icon: const Icon(Icons.share_rounded, color: Colors.white),
|
||||||
// mainAxisSize: MainAxisSize.min,
|
label: ReusableTextWidget(
|
||||||
// children: [
|
text: 'Invite Friends & Earn',
|
||||||
// Container(
|
color: Colors.white,
|
||||||
// padding: const EdgeInsets.all(18),
|
fontFamily: FontConstants.fontFamily,
|
||||||
// decoration: BoxDecoration(
|
fontSize: 15,
|
||||||
// color: Colors.red.withOpacity(0.12),
|
fontWeight: FontWeight.w700,
|
||||||
// shape: BoxShape.circle,
|
),
|
||||||
// ),
|
style: ElevatedButton.styleFrom(
|
||||||
// child: const Icon(
|
backgroundColor: ColorConstants.primaryColor,
|
||||||
// Icons.info_outline,
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
// color: Colors.red,
|
shape: RoundedRectangleBorder(
|
||||||
// size: 48,
|
borderRadius: BorderRadius.circular(12),
|
||||||
// ),
|
),
|
||||||
// ),
|
elevation: 0,
|
||||||
// const SizedBox(height: 20),
|
),
|
||||||
// const ReusableTextWidget(
|
),
|
||||||
// text: "Contacts Access Needed",
|
),
|
||||||
// fontSize: 18,
|
const SizedBox(height: 10),
|
||||||
// fontWeight: FontWeight.w600,
|
Center(
|
||||||
// fontFamily: FontConstants.fontFamily,
|
child: ReusableTextWidget(
|
||||||
// color: Colors.black,
|
text: 'Share your code or link with friends',
|
||||||
// ),
|
color: Colors.grey,
|
||||||
// const SizedBox(height: 8),
|
fontFamily: FontConstants.fontFamily,
|
||||||
// const ReusableTextWidget(
|
fontSize: 12,
|
||||||
// text:
|
fontWeight: FontWeight.w600,
|
||||||
// "Allow contacts permission to view\nand invite your friends easily.",
|
),
|
||||||
// fontSize: 14,
|
),
|
||||||
// fontWeight: FontWeight.w400,
|
],
|
||||||
// fontFamily: FontConstants.fontFamily,
|
),
|
||||||
// color: Colors.black54,
|
);
|
||||||
// textAlign: TextAlign.center,
|
}
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 24),
|
|
||||||
// SizedBox(
|
// ── How It Works ───────────────────────────────────────────────────────────
|
||||||
// width: double.infinity,
|
|
||||||
// child: ElevatedButton(
|
Widget _buildHowItWorksRow() {
|
||||||
// onPressed: permission_handler.openAppSettings,
|
final steps = [
|
||||||
// style: ElevatedButton.styleFrom(
|
{
|
||||||
// backgroundColor: Colors.red,
|
'step': 1,
|
||||||
// elevation: 0,
|
'icon': Icons.share_rounded,
|
||||||
// padding: const EdgeInsets.symmetric(vertical: 14),
|
'label': 'Share',
|
||||||
// shape: RoundedRectangleBorder(
|
'color': ColorConstants.primaryColor,
|
||||||
// borderRadius: BorderRadius.circular(12),
|
},
|
||||||
// ),
|
{
|
||||||
// ),
|
'step': 2,
|
||||||
// child: const ReusableTextWidget(
|
'icon': Icons.download_rounded,
|
||||||
// text: "Open Settings",
|
'label': 'Install',
|
||||||
// fontSize: 15,
|
'color': const Color(0xFF3498DB),
|
||||||
// fontWeight: FontWeight.w600,
|
},
|
||||||
// fontFamily: FontConstants.fontFamily,
|
{
|
||||||
// color: Colors.white,
|
'step': 3,
|
||||||
// ),
|
'icon': Icons.how_to_reg_rounded,
|
||||||
// ),
|
'label': 'Register',
|
||||||
// ),
|
'color': const Color(0xFF2ECC71),
|
||||||
// ],
|
},
|
||||||
// ),
|
{
|
||||||
// ),
|
'step': 4,
|
||||||
// ),
|
'icon': Icons.card_giftcard_rounded,
|
||||||
// ),
|
'label': 'Reward',
|
||||||
//
|
'color': const Color(0xFFE67E22),
|
||||||
// if (_contacts.isNotEmpty && !_loading && !_permissionDenied)
|
},
|
||||||
// Expanded(
|
];
|
||||||
// child: RefreshIndicator(
|
|
||||||
// onRefresh: _loadContacts,
|
return Row(
|
||||||
// child: ListView.builder(
|
children: steps.map((s) {
|
||||||
// itemCount: _contacts.length,
|
final color = s['color'] as Color;
|
||||||
// itemBuilder: (context, index) {
|
|
||||||
// final contact = _contacts[index];
|
return Expanded(
|
||||||
// final phones =
|
child: Container(
|
||||||
// contact.phones.map((p) => p.number).toList();
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
// final subtitle = phones.length > 1
|
padding: const EdgeInsets.symmetric(
|
||||||
// ? phones.sublist(0, 2).join(", ")
|
vertical: 10,
|
||||||
// : phones.first;
|
horizontal: 4,
|
||||||
//
|
),
|
||||||
// return ListTile(
|
decoration: BoxDecoration(
|
||||||
// leading: _buildAvatar(contact),
|
color: Colors.white,
|
||||||
// title: ReusableTextWidget(
|
borderRadius: BorderRadius.circular(14),
|
||||||
// text: contact.displayName.isEmpty
|
boxShadow: [
|
||||||
// ? "No Name"
|
BoxShadow(
|
||||||
// : contact.displayName,
|
color: Colors.black.withOpacity(0.05),
|
||||||
// fontSize: 14,
|
blurRadius: 10,
|
||||||
// fontWeight: FontWeight.w600,
|
offset: const Offset(0, 3),
|
||||||
// fontFamily: FontConstants.fontFamily,
|
),
|
||||||
// color: Colors.black,
|
],
|
||||||
// ),
|
),
|
||||||
// subtitle: ReusableTextWidget(
|
child: Column(
|
||||||
// text: subtitle,
|
children: [
|
||||||
// fontSize: 13,
|
Stack(
|
||||||
// fontWeight: FontWeight.w400,
|
clipBehavior: Clip.none,
|
||||||
// fontFamily: FontConstants.fontFamily,
|
children: [
|
||||||
// color: Colors.grey,
|
Container(
|
||||||
// ),
|
width: 42,
|
||||||
// trailing: TextButton(
|
height: 42,
|
||||||
// onPressed: () => _inviteWhatsApp(contact),
|
decoration: BoxDecoration(
|
||||||
// child: const ReusableTextWidget(
|
color: color.withOpacity(0.12),
|
||||||
// text: "Invite",
|
shape: BoxShape.circle,
|
||||||
// fontSize: 14,
|
),
|
||||||
// fontWeight: FontWeight.bold,
|
child: Icon(
|
||||||
// fontFamily: FontConstants.fontFamily,
|
s['icon'] as IconData,
|
||||||
// color: Colors.green,
|
color: color,
|
||||||
// ),
|
size: 20,
|
||||||
// ),
|
),
|
||||||
// onTap: () => _openWhatsApp(contact),
|
),
|
||||||
// );
|
Positioned(
|
||||||
// },
|
top: -3,
|
||||||
// ),
|
left: -3,
|
||||||
// ),
|
child: Container(
|
||||||
// ),
|
width: 16,
|
||||||
//
|
height: 16,
|
||||||
// if (_contacts.isEmpty && !_loading && !_permissionDenied)
|
decoration: BoxDecoration(
|
||||||
// const Expanded(
|
color: color,
|
||||||
// child: Center(
|
shape: BoxShape.circle,
|
||||||
// child: ReusableTextWidget(
|
),
|
||||||
// text: "No contacts found with phone numbers",
|
child: Center(
|
||||||
// fontSize: 16,
|
child: Text(
|
||||||
// fontWeight: FontWeight.w400,
|
'${s['step']}',
|
||||||
// fontFamily: FontConstants.fontFamily,
|
style: const TextStyle(
|
||||||
// color: Colors.grey,
|
color: Colors.white,
|
||||||
// ),
|
fontSize: 8,
|
||||||
// ),
|
fontWeight: FontWeight.w700,
|
||||||
// ),
|
),
|
||||||
// ],
|
),
|
||||||
// ),
|
),
|
||||||
// ),
|
),
|
||||||
// ),
|
),
|
||||||
// );
|
],
|
||||||
// }
|
),
|
||||||
// }
|
const SizedBox(height: 8),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: s['label'] as String,
|
||||||
|
color: const Color(0xFF555555),
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -13,7 +14,8 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../constants/color_constants.dart';
|
import '../../constants/color_constants.dart';
|
||||||
import '../../constants/font_constants.dart';
|
import '../../constants/font_constants.dart';
|
||||||
import '../../controllers/tenant_controller /tenant_list.dart';
|
import '../../controllers/tenant_list/tenant_controller.dart';
|
||||||
|
import '../../service/firebase_analytics/analytics_service.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
import '../home_view.dart';
|
import '../home_view.dart';
|
||||||
|
|
||||||
@@ -114,6 +116,19 @@ class _CustomerCreateViewState extends State<CustomerCreateView> {
|
|||||||
await prefs.setString('customerDoorNo', details['doorno'] ?? '');
|
await prefs.setString('customerDoorNo', details['doorno'] ?? '');
|
||||||
|
|
||||||
debugPrint("✅ Customer info saved to SharedPreferences.");
|
debugPrint("✅ Customer info saved to SharedPreferences.");
|
||||||
|
|
||||||
|
await FirebaseAnalytics.instance.setUserId(
|
||||||
|
id: customerIdStr,
|
||||||
|
);
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'signup_success',
|
||||||
|
parameters: {
|
||||||
|
'customer_id': int.tryParse(customerIdStr) ?? 0,
|
||||||
|
'customer_name': details['firstname'] ?? '',
|
||||||
|
'user_type': 'new_user',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
tenantController.loadTenants();
|
tenantController.loadTenants();
|
||||||
|
|
||||||
@@ -131,11 +146,7 @@ class _CustomerCreateViewState extends State<CustomerCreateView> {
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
// ❌ Handle failure message from API
|
// ❌ Handle failure message from API
|
||||||
debugPrint("❌ API returned failure: $message");
|
debugPrint("❌ API returned failure: $message");
|
||||||
|
|
||||||
@@ -147,8 +158,6 @@ class _CustomerCreateViewState extends State<CustomerCreateView> {
|
|||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
debugPrint(" Something went wrong");
|
debugPrint(" Something went wrong");
|
||||||
@@ -161,17 +170,11 @@ class _CustomerCreateViewState extends State<CustomerCreateView> {
|
|||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final width = size.width;
|
final width = size.width;
|
||||||
final height = size.height;
|
final height = size.height;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import '../../controllers/authentication/auth_controller.dart';
|
import '../../controllers/authentication/auth_controller.dart';
|
||||||
|
import '../../service/firebase_analytics/analytics_service.dart';
|
||||||
import '../authentication/verification_view.dart';
|
import '../authentication/verification_view.dart';
|
||||||
|
|
||||||
class Login_view extends StatelessWidget {
|
class Login_view extends StatelessWidget {
|
||||||
@@ -319,8 +320,13 @@ class Login_view extends StatelessWidget {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
onPressed: canProceed
|
onPressed: canProceed
|
||||||
? () =>
|
? () async {
|
||||||
authController.signIn(context, phone)
|
await AnalyticsService.logEvent(
|
||||||
|
'login_continue_clicked',
|
||||||
|
);
|
||||||
|
|
||||||
|
authController.signIn(context, phone);
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
child: authController.isLoading.value
|
child: authController.isLoading.value
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
|
|||||||
@@ -347,13 +347,39 @@ class _CartPageState extends State<CartPage> {
|
|||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.all(6.0),
|
padding: EdgeInsets.all(6.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'No slots available for today.\nPlease try changing dates to schedule orders.',
|
'Your cart is empty.\nBrowse our products to get started.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 46,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Switch to the Stores tab (index 1) via the shared GetX controller
|
||||||
|
Get.find<BottomNavController>().currentIndex.value = 1;
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: ColorConstants.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Browse Products',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../constants/font_constants.dart';
|
|||||||
import '../../controllers/cart_controller/cart.dart';
|
import '../../controllers/cart_controller/cart.dart';
|
||||||
import '../../controllers/order_controller/create_order_controller.dart';
|
import '../../controllers/order_controller/create_order_controller.dart';
|
||||||
import '../../modules/orders/create_order.dart';
|
import '../../modules/orders/create_order.dart';
|
||||||
|
import '../../service/firebase_analytics/analytics_service.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
import '../orders/order_succes.dart';
|
import '../orders/order_succes.dart';
|
||||||
|
|
||||||
@@ -99,12 +100,25 @@ class _OrderCountdownPageState extends State<OrderCountdownPage>
|
|||||||
|
|
||||||
|
|
||||||
if (!widget.orderCtrl.isLoading.value) {
|
if (!widget.orderCtrl.isLoading.value) {
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'order_placed',
|
||||||
|
parameters: {
|
||||||
|
'customer_id': widget.order.customerid ?? 0,
|
||||||
|
'tenant_id': widget.order.tenantid ?? 0,
|
||||||
|
'item_count': widget.order.items?.length ?? 0,
|
||||||
|
'payment_type': widget.order.paymenttype ?? 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Get.offAll(() => OrderSuccessView());
|
Get.offAll(() => OrderSuccessView());
|
||||||
widget.cartCtrl.clearCart();
|
widget.cartCtrl.clearCart();
|
||||||
await widget.cartCtrl.notifyAdmin(
|
await widget.cartCtrl.notifyAdmin(
|
||||||
title: 'Nearle Deals - New Order',
|
title: 'Nearle Deals - New Order',
|
||||||
body: 'A new order has been placed successfully by ${widget.customerName}!',
|
body: 'A new order has been placed successfully by ${widget.customerName}!',
|
||||||
);
|
);
|
||||||
|
print('jeee');
|
||||||
|
print(widget.order.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1625
lib/view/dashboard_view/ai_search.dart
Normal file
1625
lib/view/dashboard_view/ai_search.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:animated_text_kit/animated_text_kit.dart';
|
import 'package:animated_text_kit/animated_text_kit.dart';
|
||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
@@ -27,8 +28,12 @@ import '../../controllers/cart_controller/cart.dart';
|
|||||||
import '../../controllers/dashboard_controller/category.dart';
|
import '../../controllers/dashboard_controller/category.dart';
|
||||||
import '../../controllers/dashboard_controller/dashboard_controller.dart';
|
import '../../controllers/dashboard_controller/dashboard_controller.dart';
|
||||||
import '../../controllers/product/product_controller.dart';
|
import '../../controllers/product/product_controller.dart';
|
||||||
import '../../controllers/tenant_controller /tenant_list.dart';
|
import '../../controllers/tenant_list/tenant_controller.dart';
|
||||||
import '../../domain/repository/authentication/auth_repository.dart';
|
import '../../domain/repository/authentication/auth_repository.dart';
|
||||||
|
import '../../features/Products/product_card.dart';
|
||||||
|
import '../../modules/search_model/search_model.dart';
|
||||||
|
import '../../service/firebase_analytics/analytics_service.dart';
|
||||||
|
import '../../widgets/fav_button.dart';
|
||||||
import '../../widgets/tenantcategory.dart';
|
import '../../widgets/tenantcategory.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
import '../account/demo.dart';
|
import '../account/demo.dart';
|
||||||
@@ -37,8 +42,10 @@ import '../cart/cart_view.dart';
|
|||||||
import '../home_view.dart';
|
import '../home_view.dart';
|
||||||
import '../product/category_products.dart';
|
import '../product/category_products.dart';
|
||||||
import '../product/product_view.dart';
|
import '../product/product_view.dart';
|
||||||
|
import '../product/products_list.dart' hide ProductItem;
|
||||||
import '../product/tenant_products.dart';
|
import '../product/tenant_products.dart';
|
||||||
import '../qr_scaner/qr_scaner.dart';
|
import '../qr_scaner/qr_scaner.dart';
|
||||||
|
import 'ai_search.dart';
|
||||||
|
|
||||||
class DashboardPage extends StatefulWidget {
|
class DashboardPage extends StatefulWidget {
|
||||||
const DashboardPage({super.key});
|
const DashboardPage({super.key});
|
||||||
@@ -72,6 +79,87 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
static const _kProfile = 'cached_profile';
|
static const _kProfile = 'cached_profile';
|
||||||
|
|
||||||
|
|
||||||
|
final recommendations = [
|
||||||
|
RecommendedItem(
|
||||||
|
title: 'Cheapest Pizza\nNearby',
|
||||||
|
subtitle: 'From ₹149',
|
||||||
|
image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS9b5A0dGeZ4PR6cdpuvDyoMa3lO7JBRyMo8Q&s',
|
||||||
|
bgColor: Color(0xFFFFF3EC),
|
||||||
|
accentColor: Color(0xFFFF6B2C),
|
||||||
|
),
|
||||||
|
RecommendedItem(
|
||||||
|
title: 'Grocery Basket\n₹320 Less',
|
||||||
|
subtitle: 'Save more on your order',
|
||||||
|
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?w=200&fit=crop',
|
||||||
|
bgColor: Color(0xFFF0FAF0),
|
||||||
|
accentColor: Color(0xFF22A45D),
|
||||||
|
),
|
||||||
|
RecommendedItem(
|
||||||
|
title: 'Birthday\nComing Up!',
|
||||||
|
subtitle: 'Let Nearle plan it',
|
||||||
|
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=200&fit=crop',
|
||||||
|
bgColor: Color(0xFFFDF0F8),
|
||||||
|
accentColor: Color(0xFFD63384),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final products = [
|
||||||
|
ProductItem(
|
||||||
|
name: 'Chicken Biryani',
|
||||||
|
category: 'Food · HotPot',
|
||||||
|
price: '₹189',
|
||||||
|
originalPrice: '₹240',
|
||||||
|
image: 'https://images.unsplash.com/photo-1563379091339-03b21ab4a4f8?w=300&fit=crop',
|
||||||
|
rating: 4.8,
|
||||||
|
reviewCount: 320,
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Fresh Veggie Basket',
|
||||||
|
category: 'Grocery · BigBasket',
|
||||||
|
price: '₹299',
|
||||||
|
originalPrice: '₹420',
|
||||||
|
image: 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=300&fit=crop',
|
||||||
|
rating: 4.5,
|
||||||
|
reviewCount: 180,
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Margherita Pizza',
|
||||||
|
category: 'Food · Dominos',
|
||||||
|
price: '₹149',
|
||||||
|
originalPrice: '₹199',
|
||||||
|
image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=300&fit=crop',
|
||||||
|
rating: 4.6,
|
||||||
|
reviewCount: 512,
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Birthday Cake',
|
||||||
|
category: 'Bakery · CakeZone',
|
||||||
|
price: '₹549',
|
||||||
|
originalPrice: '₹699',
|
||||||
|
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&fit=crop',
|
||||||
|
rating: 4.9,
|
||||||
|
reviewCount: 94,
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Mango Smoothie',
|
||||||
|
category: 'Drinks · Juice Bar',
|
||||||
|
price: '₹89',
|
||||||
|
originalPrice: '₹120',
|
||||||
|
image: 'https://images.unsplash.com/photo-1623065422902-30a2d299bbe4?w=300&fit=crop',
|
||||||
|
rating: 4.4,
|
||||||
|
reviewCount: 67,
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Paneer Butter Masala',
|
||||||
|
category: 'Food · Behrouz',
|
||||||
|
price: '₹220',
|
||||||
|
originalPrice: '₹280',
|
||||||
|
image: 'https://images.unsplash.com/photo-1631452180519-c014fe946bc7?w=300&fit=crop',
|
||||||
|
rating: 4.7,
|
||||||
|
reviewCount: 210,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
bool status = true;
|
bool status = true;
|
||||||
bool _showBackToTop = false;
|
bool _showBackToTop = false;
|
||||||
RxBool showMiniCart = false.obs;
|
RxBool showMiniCart = false.obs;
|
||||||
@@ -84,177 +172,6 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
Icons.shopping_cart_rounded,
|
Icons.shopping_cart_rounded,
|
||||||
];
|
];
|
||||||
|
|
||||||
void _openCategoryBottomSheet(
|
|
||||||
BuildContext context,
|
|
||||||
List subCategories,
|
|
||||||
item,
|
|
||||||
) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (_) {
|
|
||||||
return SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
child: Container(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.85,
|
|
||||||
color: Colors.white,
|
|
||||||
child: Column(
|
|
||||||
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// drag handle (smaller)
|
|
||||||
Container(
|
|
||||||
width: 36,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
// Title (not oversized)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 14),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
"Category",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
Expanded(
|
|
||||||
child: subCategories.isEmpty
|
|
||||||
? const Center(
|
|
||||||
child: Text("No products available"),
|
|
||||||
)
|
|
||||||
: GridView.builder(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.fromLTRB(12, 8, 12, 16),
|
|
||||||
gridDelegate:
|
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
childAspectRatio: 0.78,
|
|
||||||
crossAxisSpacing: 10,
|
|
||||||
mainAxisSpacing: 10,
|
|
||||||
),
|
|
||||||
itemCount: subCategories.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final product = subCategories[index];
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(14),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Get.to(
|
|
||||||
() => SubCategoryProductsScreen(
|
|
||||||
tenantId: item.tenantid!,
|
|
||||||
locationId: item.locationid!,
|
|
||||||
categoryId: item.categoryid!,
|
|
||||||
tenantName: item.tenantname!,
|
|
||||||
locationname:
|
|
||||||
item.locationname!,
|
|
||||||
tenantLocation: item.suburb!,
|
|
||||||
tenantImage:
|
|
||||||
item.tenantimage!,
|
|
||||||
tenantloc:
|
|
||||||
item.locationid!,
|
|
||||||
subCategoryName:
|
|
||||||
product.subcatname
|
|
||||||
.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(14),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.black12,
|
|
||||||
width: 0.25,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black
|
|
||||||
.withOpacity(0.04),
|
|
||||||
blurRadius: 5,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
ClipOval(
|
|
||||||
child: Image.network(
|
|
||||||
product.image ?? '',
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder:
|
|
||||||
(_, __, ___) =>
|
|
||||||
const Icon(
|
|
||||||
Icons
|
|
||||||
.image_not_supported),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6),
|
|
||||||
child: ReusableTextWidget(
|
|
||||||
text:
|
|
||||||
product.subcatname ??
|
|
||||||
'',
|
|
||||||
color: Colors.black
|
|
||||||
.withOpacity(0.7),
|
|
||||||
fontFamily:
|
|
||||||
FontConstants
|
|
||||||
.fontFamily,
|
|
||||||
fontSize: 11.5,
|
|
||||||
fontWeight:
|
|
||||||
FontWeight.w600,
|
|
||||||
textAlign:
|
|
||||||
TextAlign.center,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow
|
|
||||||
.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -659,7 +576,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
onTap: () => Get.to(() => const SearchScreen()),
|
onTap: () => Get.to(() => const SearchScreen()),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 48,
|
height: 55,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -710,6 +627,11 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
IconButton(onPressed: () => Get.to(() => const SearchPage()), icon: const Icon(Icons.mic, color: Colors.black87, size: 22)),
|
||||||
|
|
||||||
|
|
||||||
|
IconButton(onPressed: () => Get.to(() => const SearchPage()), icon: const Icon(Icons.camera_alt_outlined, color: Colors.black87, size: 22)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -811,12 +733,226 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: 12),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Recommended for You",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text("See all"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 110,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: recommendations.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = recommendations[index];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 160,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// Title + Subtitle
|
||||||
|
Positioned(
|
||||||
|
top: 14,
|
||||||
|
left: 14,
|
||||||
|
right: 14,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
height: 1.25,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
item.subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: item.accentColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Network image bottom-right
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: item.image,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
placeholder: (context, url) => const SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: Icon(
|
||||||
|
Icons.image_not_supported_rounded,
|
||||||
|
color: item.accentColor.withOpacity(0.3),
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Arrow button bottom-right corner
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
right: 10,
|
||||||
|
child: Container(
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: item.accentColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: 12),
|
||||||
|
),
|
||||||
|
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Fresh picks for you",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context,) => const ProductsList()));
|
||||||
|
},
|
||||||
|
child: const Text("See all"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: 12),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
mainAxisExtent: 270,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => ProductCard(item: products[index]),
|
||||||
|
childCount: products.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: 12),
|
||||||
|
),
|
||||||
|
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Near by Stores",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text("See all"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -898,7 +1034,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
(context, index) {
|
(context, index) {
|
||||||
final item = tenantController.tenants[index];
|
final item = tenantController.tenants[index];
|
||||||
return _ZoomOnTap(
|
return _ZoomOnTap(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
Get.to(() => ProductsScreen(
|
Get.to(() => ProductsScreen(
|
||||||
tenantId: item.tenantid!,
|
tenantId: item.tenantid!,
|
||||||
locationId: item.locationid!,
|
locationId: item.locationid!,
|
||||||
@@ -910,6 +1046,16 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
tenantloc:item.locationid!,
|
tenantloc:item.locationid!,
|
||||||
subCategoryName: "",
|
subCategoryName: "",
|
||||||
));
|
));
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'store_viewed',
|
||||||
|
parameters: {
|
||||||
|
'store_id': item.tenantid!,
|
||||||
|
'store_name': item.tenantname!,
|
||||||
|
'locatiom' : item.locationname!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12, top: 12),
|
margin: const EdgeInsets.only(bottom: 12, top: 12),
|
||||||
@@ -1063,7 +1209,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
// _openCategoryBottomSheet(
|
// _openCategoryBottomSheet(
|
||||||
// context,
|
// context,
|
||||||
// item.subcategories ?? [],
|
// item.subcategories ?? [],
|
||||||
@@ -1082,6 +1228,15 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
subCategoryName: "",
|
subCategoryName: "",
|
||||||
));
|
));
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'store_viewed',
|
||||||
|
parameters: {
|
||||||
|
'store_id': item.tenantid!,
|
||||||
|
'store_name': item.tenantname!,
|
||||||
|
'locatiom' : item.locationname!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.arrow_circle_right_outlined,
|
icon: Icon(Icons.arrow_circle_right_outlined,
|
||||||
color: Colors.black.withOpacity(0.6),
|
color: Colors.black.withOpacity(0.6),
|
||||||
@@ -1113,7 +1268,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
final product = item.subcategories![index];
|
final product = item.subcategories![index];
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
|
|
||||||
// Get.to(() => SubCategoryProductsScreen(
|
// Get.to(() => SubCategoryProductsScreen(
|
||||||
// tenantId: item.tenantid!,
|
// tenantId: item.tenantid!,
|
||||||
@@ -1139,6 +1294,15 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
subCategoryName: "",
|
subCategoryName: "",
|
||||||
));
|
));
|
||||||
|
|
||||||
|
await AnalyticsService.logEvent(
|
||||||
|
'store_viewed',
|
||||||
|
parameters: {
|
||||||
|
'store_id': item.tenantid!,
|
||||||
|
'store_name': item.tenantname!,
|
||||||
|
'locatiom' : item.locationname!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
margin:
|
margin:
|
||||||
@@ -1247,11 +1411,6 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Obx(() {
|
Obx(() {
|
||||||
if (cartController.cartItems.isEmpty) return const SizedBox();
|
if (cartController.cartItems.isEmpty) return const SizedBox();
|
||||||
final tenant = cartController.currentTenant.value;
|
final tenant = cartController.currentTenant.value;
|
||||||
@@ -1374,45 +1533,12 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
],),
|
],),
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _chip(String emoji, String label) {
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFF8F4FF),
|
|
||||||
borderRadius: BorderRadius.circular(30),
|
|
||||||
border: Border.all(color: Color(0xFFE0D4FF), width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(emoji, style: TextStyle(fontSize: 13)),
|
|
||||||
SizedBox(width: 5),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF333333)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1628,23 +1754,6 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBannerShimmer() {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey.shade300,
|
|
||||||
highlightColor: Colors.grey.shade100,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
|
||||||
height: 180,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListShimmer(BuildContext context) {
|
Widget _buildListShimmer(BuildContext context) {
|
||||||
return SliverList(
|
return SliverList(
|
||||||
@@ -1834,4 +1943,5 @@ class _ZoomOnTapState extends State<_ZoomOnTap> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,210 @@ class TenantApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// SHIMMER WIDGET
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class _ShimmerBox extends StatefulWidget {
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
const _ShimmerBox({
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
this.borderRadius = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ShimmerBox> createState() => _ShimmerBoxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShimmerBoxState extends State<_ShimmerBox>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
)..repeat();
|
||||||
|
_animation = Tween<double>(begin: -1.5, end: 1.5).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (_, __) => Container(
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment(_animation.value - 1, 0),
|
||||||
|
end: Alignment(_animation.value, 0),
|
||||||
|
colors: const [
|
||||||
|
Color(0xFFE8E8E8),
|
||||||
|
Color(0xFFF5F5F5),
|
||||||
|
Color(0xFFE8E8E8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shimmer skeleton that mimics the actual screen layout
|
||||||
|
class _StoreOverviewShimmer extends StatelessWidget {
|
||||||
|
const _StoreOverviewShimmer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Top bar
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 40, height: 40, borderRadius: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Store card shimmer
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 200, height: 22, borderRadius: 6),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_ShimmerBox(width: double.infinity, height: 13, borderRadius: 4),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_ShimmerBox(width: 160, height: 13, borderRadius: 4),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 40, height: 40, borderRadius: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_ShimmerBox(width: 40, height: 40, borderRadius: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 24, thickness: 0.5),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 20, height: 20, borderRadius: 4),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_ShimmerBox(width: 100, height: 13, borderRadius: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 24, thickness: 0.5),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 20, height: 20, borderRadius: 4),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_ShimmerBox(width: 150, height: 13, borderRadius: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Bad experience card shimmer
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 36, height: 36, borderRadius: 18),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 180, height: 13, borderRadius: 4),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_ShimmerBox(width: 140, height: 11, borderRadius: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Legal card shimmer
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: List.generate(4, (i) => Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: i < 3 ? 16.0 : 0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_ShimmerBox(width: 80, height: 11, borderRadius: 4),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_ShimmerBox(width: 160, height: 13, borderRadius: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
// SCREEN
|
// SCREEN
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
@@ -285,9 +489,16 @@ class _StoreOverviewScreenState extends State<StoreOverviewScreen> {
|
|||||||
child: FutureBuilder<TenantDetails>(
|
child: FutureBuilder<TenantDetails>(
|
||||||
future: _future,
|
future: _future,
|
||||||
builder: (context, snap) {
|
builder: (context, snap) {
|
||||||
|
// ── SHIMMER while loading ──────────────
|
||||||
if (snap.connectionState == ConnectionState.waiting) {
|
if (snap.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: const _StoreOverviewShimmer()),
|
||||||
|
_bottomButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snap.hasError) {
|
if (snap.hasError) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
560
lib/view/dashboard_view/test.dart
Normal file
560
lib/view/dashboard_view/test.dart
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// CONFIG — change IP to your Mac's local IP
|
||||||
|
// Run: ipconfig getifaddr en0
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
const String kBaseUrl = 'http://192.168.1.XXX:8000';
|
||||||
|
const int kCustomerId = 6060;
|
||||||
|
const double kLatitude = 11.0168;
|
||||||
|
const double kLongitude = 76.9558;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// API SERVICE
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class AgentService {
|
||||||
|
static Future<Map<String, dynamic>> findIngredients(String userInput) async {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse('$kBaseUrl/find-ingredients'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'user_input': userInput,
|
||||||
|
'customer_id': kCustomerId,
|
||||||
|
'latitude': kLatitude,
|
||||||
|
'longitude': kLongitude,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return jsonDecode(res.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> chat(
|
||||||
|
String message,
|
||||||
|
List conversationHistory,
|
||||||
|
) async {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse('$kBaseUrl/chat'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'message': message,
|
||||||
|
'customer_id': kCustomerId,
|
||||||
|
'latitude': kLatitude,
|
||||||
|
'longitude': kLongitude,
|
||||||
|
'conversation_history': conversationHistory,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return jsonDecode(res.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> clarify(
|
||||||
|
String originalInput,
|
||||||
|
String clarification,
|
||||||
|
) async {
|
||||||
|
final res = await http.post(
|
||||||
|
Uri.parse('$kBaseUrl/clarify'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'original_input': originalInput,
|
||||||
|
'clarification': clarification,
|
||||||
|
'customer_id': kCustomerId,
|
||||||
|
'latitude': kLatitude,
|
||||||
|
'longitude': kLongitude,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return jsonDecode(res.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// SEARCH SCREEN
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class SearchScreen extends StatefulWidget {
|
||||||
|
const SearchScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchScreen> createState() => _SearchScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchScreenState extends State<SearchScreen> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _agentMessage;
|
||||||
|
String? _clarificationQuestion;
|
||||||
|
String? _lastInput;
|
||||||
|
|
||||||
|
List _ingredients = [];
|
||||||
|
List _stores = [];
|
||||||
|
|
||||||
|
// Quick suggestion chips
|
||||||
|
final List<String> _suggestions = [
|
||||||
|
'🍓 Strawberry',
|
||||||
|
'🍗 Biryani',
|
||||||
|
'🤒 Fever medicine',
|
||||||
|
'🍳 Cook sambar',
|
||||||
|
'🥛 Milk',
|
||||||
|
'😴 Bored snacks',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Search via /find-ingredients ──────────────────────────
|
||||||
|
Future<void> _search(String input) async {
|
||||||
|
if (input.trim().isEmpty) return;
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_agentMessage = null;
|
||||||
|
_clarificationQuestion = null;
|
||||||
|
_stores = [];
|
||||||
|
_ingredients = [];
|
||||||
|
_lastInput = input;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await AgentService.findIngredients(input);
|
||||||
|
|
||||||
|
if (result['status'] == 'needs_clarification') {
|
||||||
|
setState(() {
|
||||||
|
_clarificationQuestion = result['question'];
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_agentMessage = result['agent_message'];
|
||||||
|
_ingredients = result['ingredients_needed'] ?? [];
|
||||||
|
_stores = result['stores'] ?? [];
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_agentMessage = 'Something went wrong. Please try again.';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Answer agent clarification ────────────────────────────
|
||||||
|
Future<void> _answerClarification(String answer) async {
|
||||||
|
setState(() {
|
||||||
|
_clarificationQuestion = null;
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await AgentService.clarify(_lastInput ?? '', answer);
|
||||||
|
setState(() {
|
||||||
|
_agentMessage = result['agent_message'];
|
||||||
|
_ingredients = result['ingredients_needed'] ?? [];
|
||||||
|
_stores = result['stores'] ?? [];
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_agentMessage = 'Something went wrong.';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
// BUILD
|
||||||
|
// ─────────────────────────────────────────────────────────
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Nearle',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// ── Search Bar ──────────────────────────────────
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
onSubmitted: _search,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search or ask anything...',
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||||
|
prefixIcon: const Icon(Icons.search, color: Color(0xFF6C63FF)),
|
||||||
|
filled: true,
|
||||||
|
fillColor: const Color(0xFFF0EFFF),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _search(_searchController.text),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF6C63FF),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.arrow_forward, color: Colors.white, size: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Suggestion Chips ────────────────────────────
|
||||||
|
if (_stores.isEmpty && !_isLoading && _clarificationQuestion == null)
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
itemCount: _suggestions.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
final text = _suggestions[i].replaceAll(RegExp(r'[^\w\s]'), '').trim();
|
||||||
|
_searchController.text = text;
|
||||||
|
_search(text);
|
||||||
|
},
|
||||||
|
child: Chip(
|
||||||
|
label: Text(
|
||||||
|
_suggestions[i],
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
side: const BorderSide(color: Color(0xFFE0E0E0)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// ── Body ────────────────────────────────────────
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(color: Color(0xFF6C63FF)),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Anna is finding the best deals...', style: TextStyle(color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _clarificationQuestion != null
|
||||||
|
? _buildClarification()
|
||||||
|
: _stores.isEmpty
|
||||||
|
? _buildEmpty()
|
||||||
|
: _buildResults(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clarification UI ──────────────────────────────────────
|
||||||
|
Widget _buildClarification() {
|
||||||
|
final answerController = TextEditingController();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFEDE9FF),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text('🤔', style: TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_clarificationQuestion!,
|
||||||
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: answerController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Type your answer...',
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||||
|
),
|
||||||
|
onSubmitted: _answerClarification,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _answerClarification(answerController.text),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF6C63FF),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: const Text('Send', style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empty State ───────────────────────────────────────────
|
||||||
|
Widget _buildEmpty() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('🛒', style: TextStyle(fontSize: 60)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_agentMessage ?? 'Search for anything\nfrom nearby stores',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Results ───────────────────────────────────────────────
|
||||||
|
Widget _buildResults() {
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
// Agent message
|
||||||
|
if (_agentMessage != null)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFEDE9FF),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text('✨', style: TextStyle(fontSize: 20)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(child: Text(_agentMessage!, style: const TextStyle(fontSize: 14))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Store cards
|
||||||
|
..._stores.map((storeData) => _StoreCard(storeData: storeData)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// STORE CARD WIDGET
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class _StoreCard extends StatelessWidget {
|
||||||
|
final Map storeData;
|
||||||
|
const _StoreCard({required this.storeData});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final store = storeData['store'] as Map;
|
||||||
|
final products = storeData['matched_products'] as List;
|
||||||
|
final savings = store['total_savings'] ?? 0;
|
||||||
|
final total = store['total_discounted_price'] ?? 0;
|
||||||
|
final hasOffers = store['has_offers'] == true;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2))],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Store header
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF0EFFF),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.store, color: Color(0xFF6C63FF), size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
store['tenantname'] ?? '',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
store['locationname'] ?? '',
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasOffers)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE8F5E9),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Save ₹${savings.toStringAsFixed(0)}',
|
||||||
|
style: const TextStyle(color: Color(0xFF2E7D32), fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(height: 1),
|
||||||
|
|
||||||
|
// Products
|
||||||
|
...products.map((m) => _ProductRow(matched: m)),
|
||||||
|
|
||||||
|
const Divider(height: 1),
|
||||||
|
|
||||||
|
// Total + Order button
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Total', style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||||
|
Text(
|
||||||
|
'₹${total.toStringAsFixed(2)}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color(0xFF1A1A1A)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF6C63FF),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: const Text('Order Now', style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// PRODUCT ROW WIDGET
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
class _ProductRow extends StatelessWidget {
|
||||||
|
final Map matched;
|
||||||
|
const _ProductRow({required this.matched});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final product = matched['product'] as Map;
|
||||||
|
final emoji = matched['emoji'] ?? '🛒';
|
||||||
|
final name = product['productname'] ?? matched['ingredient'] ?? '';
|
||||||
|
final price = product['discounted_price'] ?? 0;
|
||||||
|
final original = product['original_price'] ?? 0;
|
||||||
|
final hasOffer = product['has_offer'] == true;
|
||||||
|
final discount = product['discount_percent'] ?? 0;
|
||||||
|
final quantity = matched['quantity'] ?? '';
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(emoji, style: const TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(name, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||||
|
Text(quantity, style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'₹${price.toStringAsFixed(2)}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFF1A1A1A)),
|
||||||
|
),
|
||||||
|
if (hasOffer)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'₹${original.toStringAsFixed(0)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 11,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'$discount% off',
|
||||||
|
style: const TextStyle(color: Color(0xFF2E7D32), fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import 'package:lottie/lottie.dart';
|
|||||||
import 'package:nearledaily/view/qr_scaner/qr_scaner.dart';
|
import 'package:nearledaily/view/qr_scaner/qr_scaner.dart';
|
||||||
import '../constants/font_constants.dart';
|
import '../constants/font_constants.dart';
|
||||||
import '../controllers/cart_controller/cart.dart';
|
import '../controllers/cart_controller/cart.dart';
|
||||||
|
import '../features/Store/presentation/screens/store_view.dart';
|
||||||
|
import '../features/dashboard/presentation/screens/dash_view.dart';
|
||||||
import '../widgets/text_widget.dart';
|
import '../widgets/text_widget.dart';
|
||||||
import 'account/account_view.dart';
|
import 'account/account_view.dart';
|
||||||
import 'cart/cart_view.dart';
|
import 'cart/cart_view.dart';
|
||||||
@@ -22,8 +24,9 @@ const Color _kInactive = Color(0xFFCBA8E4);
|
|||||||
// ─── Screens ──────────────────────────────────────────────────────────────────
|
// ─── Screens ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
final List<Widget> _screens = [
|
final List<Widget> _screens = [
|
||||||
DashboardPage(),
|
HomeScreen(),
|
||||||
const OrdersByStoreScreen(showBackArrow: false),
|
StoreViewPage(),
|
||||||
|
// const OrdersByStoreScreen(showBackArrow: false),
|
||||||
QrScannerPage(),
|
QrScannerPage(),
|
||||||
CartPage(),
|
CartPage(),
|
||||||
AccountPage(),
|
AccountPage(),
|
||||||
@@ -131,18 +134,29 @@ class BottomNavigation extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
backgroundColor: Colors.white,
|
canPop: controller.currentIndex.value == 0,
|
||||||
extendBody: true,
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
bottomNavigationBar: Obx(
|
if (didPop) return;
|
||||||
() => _BottomNavBar(
|
|
||||||
currentIndex: controller.currentIndex.value,
|
// Not on Home tab -> go back to Home instead of exiting/popping
|
||||||
cartController: cartController,
|
if (controller.currentIndex.value != 0) {
|
||||||
onTap: (i) => controller.currentIndex.value = i,
|
controller.currentIndex.value = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
extendBody: true,
|
||||||
|
bottomNavigationBar: Obx(
|
||||||
|
() => _BottomNavBar(
|
||||||
|
currentIndex: controller.currentIndex.value,
|
||||||
|
cartController: cartController,
|
||||||
|
onTap: (i) => controller.currentIndex.value = i,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Obx(
|
||||||
|
() => _screens[controller.currentIndex.value],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
body: Obx(
|
|
||||||
() => _screens[controller.currentIndex.value],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -240,8 +254,8 @@ class _GlassPill extends StatelessWidget {
|
|||||||
onTap: () => onTap(0),
|
onTap: () => onTap(0),
|
||||||
),
|
),
|
||||||
_NavItem(
|
_NavItem(
|
||||||
icon: Icons.receipt_long_rounded,
|
icon: Icons.storefront,
|
||||||
label: 'Order',
|
label: 'Stores',
|
||||||
isActive: currentIndex == 1,
|
isActive: currentIndex == 1,
|
||||||
onTap: () => onTap(1),
|
onTap: () => onTap(1),
|
||||||
),
|
),
|
||||||
@@ -440,4 +454,4 @@ class _CartNavItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:nearledaily/constants/color_constants.dart';
|
|||||||
import '../../../constants/font_constants.dart';
|
import '../../../constants/font_constants.dart';
|
||||||
import '../../controllers/cart_controller/cart.dart';
|
import '../../controllers/cart_controller/cart.dart';
|
||||||
import '../../controllers/dashboard_controller/dashboard_controller.dart';
|
import '../../controllers/dashboard_controller/dashboard_controller.dart';
|
||||||
|
import '../../features/orders/presentation/order_tracking.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
import '../home_view.dart';
|
import '../home_view.dart';
|
||||||
|
|
||||||
@@ -233,7 +234,44 @@ class _OrderSuccessViewState extends State<OrderSuccessView>
|
|||||||
|
|
||||||
SizedBox(height: size.height * 0.015),
|
SizedBox(height: size.height * 0.015),
|
||||||
|
|
||||||
|
// Secondary CTA — Track Order
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.to(() => DeliveryTrackingPage());
|
||||||
|
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: ColorConstants.primaryColor,
|
||||||
|
side: BorderSide(
|
||||||
|
color: ColorConstants.primaryColor,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.local_shipping_outlined, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ReusableTextWidget(
|
||||||
|
text: 'Track Order',
|
||||||
|
color: ColorConstants.primaryColor,
|
||||||
|
fontFamily: FontConstants.fontFamily,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: size.height * 0.015),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../constants/asset_constants.dart';
|
|||||||
import '../../constants/color_constants.dart';
|
import '../../constants/color_constants.dart';
|
||||||
import '../../constants/font_constants.dart';
|
import '../../constants/font_constants.dart';
|
||||||
import '../../controllers/tenant/get_tenant.dart'; // OrderedTenantController
|
import '../../controllers/tenant/get_tenant.dart'; // OrderedTenantController
|
||||||
|
import '../../features/orders/presentation/order_tracking.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
import '../../widgets/text_widget.dart';
|
||||||
import 'my_orders.dart'; // OrderDatum
|
import 'my_orders.dart'; // OrderDatum
|
||||||
|
|
||||||
@@ -450,149 +451,126 @@ class _OrdersByStoreScreenState extends State<OrdersByStoreScreen>
|
|||||||
height: 5,
|
height: 5,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
MainAxisAlignment
|
|
||||||
.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
|
// Track Button
|
||||||
GestureDetector(
|
Expanded(
|
||||||
onTap: () async {
|
child: Container(
|
||||||
final uri = Uri(scheme: 'tel', path: order.pickupcontactno!);
|
height: 44,
|
||||||
if (await canLaunchUrl(uri)) {
|
margin: const EdgeInsets.only(right: 10),
|
||||||
await launchUrl(uri);
|
decoration: BoxDecoration(
|
||||||
}
|
borderRadius: BorderRadius.circular(14),
|
||||||
},
|
gradient: const LinearGradient(
|
||||||
child: Column(
|
colors: [Color(0xFF662582), Color(0xFF7A2E9C)],
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
begin: Alignment.topLeft,
|
||||||
children: [
|
end: Alignment.bottomRight,
|
||||||
ReusableTextWidget(
|
|
||||||
text: 'Contact :',
|
|
||||||
color: ColorConstants.blackColor.withOpacity(0.67),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: FontConstants.fontFamily,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
boxShadow: [
|
||||||
Row(
|
BoxShadow(
|
||||||
children: [
|
color: const Color(0xFF662582).withOpacity(0.3),
|
||||||
Icon(
|
blurRadius: 10,
|
||||||
Icons.phone_rounded,
|
offset: const Offset(0, 4),
|
||||||
size: 14,
|
),
|
||||||
color: ColorConstants.primaryColor,
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
Get.to(() => const DeliveryTrackingPage());
|
||||||
|
},
|
||||||
|
child: const Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.location_on_rounded, color: Colors.white, size: 18),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Track',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 5),
|
),
|
||||||
ReusableTextWidget(
|
|
||||||
text: order.pickupcontactno ?? "No Contact",
|
|
||||||
color: ColorConstants.primaryColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: FontConstants.fontFamily,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// View Details Button
|
||||||
|
Expanded(
|
||||||
ElevatedButton(
|
child: Container(
|
||||||
style: ElevatedButton
|
height: 44,
|
||||||
.styleFrom(
|
decoration: BoxDecoration(
|
||||||
padding:
|
borderRadius: BorderRadius.circular(14),
|
||||||
const EdgeInsets
|
border: Border.all(color: primaryColor, width: 1.4),
|
||||||
.symmetric(
|
color: Colors.white,
|
||||||
horizontal: 12,
|
|
||||||
vertical: 5),
|
|
||||||
shape:
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(
|
|
||||||
8),
|
|
||||||
),
|
),
|
||||||
backgroundColor:
|
child: Material(
|
||||||
primaryColor,
|
color: Colors.transparent,
|
||||||
),
|
child: InkWell(
|
||||||
onPressed: () {
|
borderRadius: BorderRadius.circular(14),
|
||||||
// ✨ Haptic feedback on button press
|
onTap: () {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
|
Navigator.push(
|
||||||
// ✨ Smooth page transition
|
context,
|
||||||
Navigator.push(
|
PageRouteBuilder(
|
||||||
context,
|
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||||
PageRouteBuilder(
|
OrderDetailsPage(
|
||||||
pageBuilder: (context,
|
orderId: order.orderid ?? 'Unknown',
|
||||||
animation,
|
storeName: tenantName,
|
||||||
secondaryAnimation) =>
|
storeLocation: order.tenantsuburb ?? 'Unknown',
|
||||||
OrderDetailsPage(
|
tax: order.totaltaxamount ?? 0,
|
||||||
orderId: order
|
gstno: order.gstno ?? "",
|
||||||
.orderid ??
|
fee: order.deliverycharge ?? 0,
|
||||||
'Unknown',
|
items: order.orderdetails
|
||||||
storeName: tenantName,
|
?.map((item) => {
|
||||||
storeLocation: order
|
'name': item.productname ?? 'Unknown',
|
||||||
.tenantsuburb ??
|
'quantity': item.orderqty ?? 0,
|
||||||
'Unknown',
|
'productSumPrice': item.productsumprice ?? 0.0,
|
||||||
tax: order
|
'price': item.price ?? 0.0,
|
||||||
.totaltaxamount ??
|
'discountamount': item.price ?? 0.0,
|
||||||
0,
|
'image': item.productimage ?? '',
|
||||||
gstno: order.gstno ?? "",
|
})
|
||||||
fee: order
|
.toList() ??
|
||||||
.deliverycharge ??
|
[],
|
||||||
0,
|
),
|
||||||
items: order
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
.orderdetails
|
return FadeTransition(
|
||||||
?.map((item) =>
|
opacity: animation,
|
||||||
{
|
child: SlideTransition(
|
||||||
'name':
|
position: Tween<Offset>(
|
||||||
item.productname ?? 'Unknown',
|
begin: const Offset(0.05, 0),
|
||||||
'quantity':
|
end: Offset.zero,
|
||||||
item.orderqty ?? 0,
|
).animate(animation),
|
||||||
'productSumPrice':
|
child: child,
|
||||||
item.productsumprice ?? 0.0,
|
),
|
||||||
'price':
|
);
|
||||||
item.price ?? 0.0,
|
},
|
||||||
'discountamount':
|
transitionDuration: const Duration(milliseconds: 300),
|
||||||
item.price ?? 0.0,
|
|
||||||
'image':
|
|
||||||
item.productimage ?? '',
|
|
||||||
})
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
transitionsBuilder:
|
|
||||||
(context,
|
|
||||||
animation,
|
|
||||||
secondaryAnimation,
|
|
||||||
child) {
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child:
|
|
||||||
SlideTransition(
|
|
||||||
position:
|
|
||||||
Tween<Offset>(
|
|
||||||
begin:
|
|
||||||
const Offset(
|
|
||||||
0.05, 0),
|
|
||||||
end:
|
|
||||||
Offset.zero,
|
|
||||||
).animate(
|
|
||||||
animation),
|
|
||||||
child: child,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
transitionDuration:
|
child: Center(
|
||||||
const Duration(
|
child: Text(
|
||||||
milliseconds:
|
'View Details',
|
||||||
300),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: primaryColor,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"View Details",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
886
lib/view/product/product_details.dart
Normal file
886
lib/view/product/product_details.dart
Normal file
@@ -0,0 +1,886 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:nearledaily/view/product/products_list.dart';
|
||||||
|
|
||||||
|
// ─── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void showProductSheet(BuildContext context, ProductItem item) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
barrierColor: Colors.black.withOpacity(0.4),
|
||||||
|
builder: (_) => _ProductSheet(item: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sheet widget ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _ProductSheet extends StatefulWidget {
|
||||||
|
final ProductItem item;
|
||||||
|
const _ProductSheet({required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ProductSheet> createState() => _ProductSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductSheetState extends State<_ProductSheet> {
|
||||||
|
final DraggableScrollableController _drag = DraggableScrollableController();
|
||||||
|
|
||||||
|
static const double _minSize = 0.54;
|
||||||
|
static const double _maxSize = 1.0;
|
||||||
|
|
||||||
|
int _qty = 1;
|
||||||
|
bool _isFav = false;
|
||||||
|
bool _inCart = false;
|
||||||
|
double _expansion = 0.0;
|
||||||
|
|
||||||
|
static const Color _purple = Color(0xFF662582);
|
||||||
|
static const Color _purpleLight = Color(0xFFF0EBFF);
|
||||||
|
static const Color _dark = Color(0xFF111111);
|
||||||
|
static const Color _surface = Color(0xFFF8F8F8);
|
||||||
|
static const Color _muted = Color(0xFF888888);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_drag.addListener(_onDrag);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDrag() {
|
||||||
|
if (!_drag.isAttached) return;
|
||||||
|
final t = ((_drag.size - _minSize) / (_maxSize - _minSize)).clamp(0.0, 1.0);
|
||||||
|
if (t > 0.88) {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
|
||||||
|
} else {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
|
||||||
|
}
|
||||||
|
setState(() => _expansion = t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
|
||||||
|
_drag.removeListener(_onDrag);
|
||||||
|
_drag.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get _discount {
|
||||||
|
final orig = double.tryParse(
|
||||||
|
widget.item.originalPrice.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0;
|
||||||
|
final cur = double.tryParse(
|
||||||
|
widget.item.price.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0;
|
||||||
|
return orig > 0 ? ((orig - cur) / orig * 100).round() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _savedAmount {
|
||||||
|
final orig = double.tryParse(
|
||||||
|
widget.item.originalPrice.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0;
|
||||||
|
final cur = double.tryParse(
|
||||||
|
widget.item.price.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0;
|
||||||
|
final saved = orig - cur;
|
||||||
|
return saved > 0 ? '₹${saved.toStringAsFixed(0)}' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snapToFull() => _drag.animateTo(_maxSize,
|
||||||
|
duration: const Duration(milliseconds: 400), curve: Curves.easeOutCubic);
|
||||||
|
|
||||||
|
void _snapToPeek() => _drag.animateTo(_minSize,
|
||||||
|
duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final topPad = MediaQuery.of(context).padding.top;
|
||||||
|
final botPad = MediaQuery.of(context).padding.bottom;
|
||||||
|
final e = _expansion;
|
||||||
|
final cornerRadius = lerpDouble(24.0, 0.0, e)!;
|
||||||
|
final imageHeight = lerpDouble(300.0, topPad + 340.0, e)!;
|
||||||
|
final discount = _discount;
|
||||||
|
final saved = _savedAmount;
|
||||||
|
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
controller: _drag,
|
||||||
|
initialChildSize: _minSize,
|
||||||
|
minChildSize: _minSize,
|
||||||
|
maxChildSize: _maxSize,
|
||||||
|
snap: true,
|
||||||
|
snapSizes: const [_minSize, _maxSize],
|
||||||
|
builder: (context, scrollCtrl) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.vertical(top: Radius.circular(cornerRadius)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollCtrl,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// ── Hero ──
|
||||||
|
_buildHero(imageHeight, cornerRadius, topPad, e),
|
||||||
|
|
||||||
|
// ── Drag handle (peek) ──
|
||||||
|
if (e < 0.2)
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFDDD8F0),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Body ──
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Tag chip
|
||||||
|
_tagChip(widget.item.tag),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Product name
|
||||||
|
Text(
|
||||||
|
widget.item.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _dark,
|
||||||
|
letterSpacing: -0.4,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
// Category / store
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF4CAF50),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
widget.item.category,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _muted,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Price
|
||||||
|
_buildPriceRow(discount, saved),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
if (saved.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
'You save $saved · Free delivery on this order',
|
||||||
|
style: const TextStyle(fontSize: 12, color: _muted),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Rating
|
||||||
|
_buildRatingRow(),
|
||||||
|
|
||||||
|
// Swipe hint
|
||||||
|
if (e < 0.12) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_swipeHint(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Expanded content
|
||||||
|
if (e > 0.12) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Highlights
|
||||||
|
AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
opacity: e > 0.4 ? 1.0 : 0.0,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_sectionLabel('Why you\'ll love it'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildHighlights(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// About (derived from name + category)
|
||||||
|
AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
opacity: e > 0.52 ? 1.0 : 0.0,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_sectionLabel('About'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'A premium quality ${widget.item.name} carefully '
|
||||||
|
'curated for you. Sourced from trusted partners at '
|
||||||
|
'${widget.item.category}. Every order is freshness-'
|
||||||
|
'guaranteed with no compromise on quality.',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Color(0xFF555555),
|
||||||
|
height: 1.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Reviews
|
||||||
|
AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
opacity: e > 0.62 ? 1.0 : 0.0,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDivider(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildReviewsHeader(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildReviewCard(
|
||||||
|
initials: 'PR',
|
||||||
|
name: 'Priya R.',
|
||||||
|
date: '2 days ago',
|
||||||
|
stars: 5,
|
||||||
|
text:
|
||||||
|
'Absolutely love it! Super fresh and delivered on time. Will definitely order again.',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildReviewCard(
|
||||||
|
initials: 'AK',
|
||||||
|
name: 'Arjun K.',
|
||||||
|
date: '1 week ago',
|
||||||
|
stars: 5,
|
||||||
|
text:
|
||||||
|
'Great quality and packaging. Exactly as described. Fast delivery too!',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Quantity
|
||||||
|
AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
opacity: e > 0.70 ? 1.0 : 0.0,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDivider(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildQuantityRow(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: botPad + 90),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── CTA bar ──
|
||||||
|
_buildCtaBar(botPad),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hero ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildHero(
|
||||||
|
double imageHeight, double cornerRadius, double topPad, double e) {
|
||||||
|
final item = widget.item;
|
||||||
|
final discount = _discount;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// Product image
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(cornerRadius),
|
||||||
|
topRight: Radius.circular(cornerRadius),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
height: imageHeight,
|
||||||
|
width: double.infinity,
|
||||||
|
color: item.bgColor,
|
||||||
|
child: item.imageUrl.isNotEmpty
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: item.imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (_, __) => const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: _purple),
|
||||||
|
),
|
||||||
|
errorWidget: (_, __, ___) => Image.network(
|
||||||
|
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQfoIZCZOkJq4Yg00gVdyXr49xxOJsFjSjABE_SAzOiDQ&s=10',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Back / close button (full screen)
|
||||||
|
// Positioned(
|
||||||
|
// top: lerpDouble(-60, topPad + 12, e)!,
|
||||||
|
// left: 14,
|
||||||
|
// child: AnimatedOpacity(
|
||||||
|
// opacity: e > 0.7 ? 1.0 : 0.0,
|
||||||
|
// duration: const Duration(milliseconds: 150),
|
||||||
|
// child: _circleButton(
|
||||||
|
// icon: Icons.arrow_back_ios_new_rounded,
|
||||||
|
// onTap: () {
|
||||||
|
// HapticFeedback.lightImpact();
|
||||||
|
// if (_drag.isAttached && _drag.size > _minSize + 0.05) {
|
||||||
|
// _snapToPeek();
|
||||||
|
// } else {
|
||||||
|
// Navigator.pop(context);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// Share + search (full screen)
|
||||||
|
|
||||||
|
|
||||||
|
// Discount badge
|
||||||
|
if (discount > 0)
|
||||||
|
Positioned(
|
||||||
|
bottom: 48,
|
||||||
|
left: 16,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _dark,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$discount% OFF',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tag chip ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _tagChip(String label) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _purpleLight,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _purple,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Price ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildPriceRow(int discount, String saved) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.item.price,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _dark,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// Text(
|
||||||
|
// widget.item.originalPrice,
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 14,
|
||||||
|
// color: Color(0xFFBBBBBB),
|
||||||
|
// decoration: TextDecoration.lineThrough,
|
||||||
|
// decorationColor: Color(0xFFBBBBBB),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
if (discount > 0) ...[
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _dark,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$discount% OFF',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rating ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildRatingRow() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: Color(0xFFF0F0F0)),
|
||||||
|
bottom: BorderSide(color: Color(0xFFF0F0F0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: List.generate(
|
||||||
|
5,
|
||||||
|
(i) => Icon(
|
||||||
|
i < widget.item.rating.floor()
|
||||||
|
? Icons.star_rounded
|
||||||
|
: Icons.star_border_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.yellow.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
widget.item.rating.toStringAsFixed(1),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13, fontWeight: FontWeight.w700, color: _dark),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
// reviewCount is int — convert directly
|
||||||
|
Text(
|
||||||
|
'· ${widget.item.reviewCount} ratings',
|
||||||
|
style: const TextStyle(fontSize: 12, color: _muted),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Highlights ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildHighlights() {
|
||||||
|
final tiles = [
|
||||||
|
_HighlightData(
|
||||||
|
icon: Icons.bolt_rounded,
|
||||||
|
label: 'Fast\nDelivery',
|
||||||
|
sub: '20–45 min',
|
||||||
|
iconColor: _purple,
|
||||||
|
),
|
||||||
|
_HighlightData(
|
||||||
|
icon: Icons.verified_rounded,
|
||||||
|
label: 'Quality\nVerified',
|
||||||
|
sub: 'Trusted partners',
|
||||||
|
iconColor: const Color(0xFF1B5E20),
|
||||||
|
),
|
||||||
|
_HighlightData(
|
||||||
|
icon: Icons.refresh_rounded,
|
||||||
|
label: 'Easy\nReturns',
|
||||||
|
sub: 'Hassle-free',
|
||||||
|
iconColor: const Color(0xFFB45309),
|
||||||
|
),
|
||||||
|
_HighlightData(
|
||||||
|
icon: Icons.lock_outline_rounded,
|
||||||
|
label: 'Secure\nPay',
|
||||||
|
sub: '100% safe',
|
||||||
|
iconColor: const Color(0xFF1565C0),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: List.generate(tiles.length, (index) {
|
||||||
|
final t = tiles[index];
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.only(right: index < tiles.length - 1 ? 8 : 0),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _surface,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(t.icon, color: t.iconColor, size: 20),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
t.label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: t.iconColor,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
t.sub,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 9, color: _muted, height: 1.3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reviews ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildReviewsHeader() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Customer reviews',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, fontWeight: FontWeight.w700, color: _dark),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// reviewCount is int
|
||||||
|
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReviewCard({
|
||||||
|
required String initials,
|
||||||
|
required String name,
|
||||||
|
required String date,
|
||||||
|
required int stars,
|
||||||
|
required String text,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _surface,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 15,
|
||||||
|
backgroundColor: _purpleLight,
|
||||||
|
child: Text(
|
||||||
|
initials,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _purple),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _dark)),
|
||||||
|
Row(
|
||||||
|
children: List.generate(
|
||||||
|
stars,
|
||||||
|
(_) => const Icon(Icons.star_rounded,
|
||||||
|
size: 11, color: _dark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(date,
|
||||||
|
style: const TextStyle(fontSize: 11, color: _muted)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: Color(0xFF666666), height: 1.6),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Quantity ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildQuantityRow() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
_sectionLabel('Quantity'),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_qtyButton(
|
||||||
|
icon: Icons.remove_rounded,
|
||||||
|
onTap: () {
|
||||||
|
if (_qty > 1) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() => _qty--);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filled: false,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
transitionBuilder: (child, anim) =>
|
||||||
|
ScaleTransition(scale: anim, child: child),
|
||||||
|
child: Text(
|
||||||
|
'$_qty',
|
||||||
|
key: ValueKey(_qty),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16, fontWeight: FontWeight.w700, color: _dark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_qtyButton(
|
||||||
|
icon: Icons.add_rounded,
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() => _qty++);
|
||||||
|
},
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CTA bar ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildCtaBar(double botPad) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(20, 14, 20, botPad + 16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border(top: BorderSide(color: Color(0xFFF0F0F0))),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Wishlist
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() => _isFav = !_isFav);
|
||||||
|
},
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isFav ? const Color(0xFFFFF5F5) : Colors.white,
|
||||||
|
border: Border.all(
|
||||||
|
color: _isFav
|
||||||
|
? const Color(0xFFFFD0D0)
|
||||||
|
: const Color(0xFFE8E8E8),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_isFav
|
||||||
|
? Icons.favorite_rounded
|
||||||
|
: Icons.favorite_border_rounded,
|
||||||
|
color: _isFav ? const Color(0xFFE24B4A) : _dark,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
|
||||||
|
// Add to bag
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
setState(() => _inCart = true);
|
||||||
|
_snapToFull();
|
||||||
|
},
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _inCart ? const Color(0xFF2E2E2E) : _dark,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
_inCart ? 'Added ✓' : 'Add to cart',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Swipe hint ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _swipeHint() {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _snapToFull,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: _purple.withOpacity(0.2)),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Swipe up for full details',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13, fontWeight: FontWeight.w600, color: _purple),
|
||||||
|
),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Icon(Icons.keyboard_arrow_up_rounded, color: _purple, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildDivider() =>
|
||||||
|
Container(height: 1, color: const Color(0xFFF0F0F0));
|
||||||
|
|
||||||
|
Widget _sectionLabel(String label) => Text(
|
||||||
|
label.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: _muted,
|
||||||
|
letterSpacing: 1.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _circleButton({required IconData icon, required VoidCallback onTap}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.85),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 18, color: _dark),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _qtyButton(
|
||||||
|
{required IconData icon,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
required bool filled}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: filled ? _dark : Colors.white,
|
||||||
|
border:
|
||||||
|
Border.all(color: filled ? _dark : const Color(0xFFE8E8E8)),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 18, color: filled ? Colors.white : _dark),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Internal data class ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _HighlightData {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String sub;
|
||||||
|
final Color iconColor;
|
||||||
|
const _HighlightData({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.sub,
|
||||||
|
required this.iconColor,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,14 +3,11 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:readmore/readmore.dart';
|
|
||||||
import '../../constants/color_constants.dart';
|
import '../../constants/color_constants.dart';
|
||||||
import '../../constants/font_constants.dart';
|
|
||||||
import '../../controllers/cart_controller/cart.dart';
|
import '../../controllers/cart_controller/cart.dart';
|
||||||
import '../../controllers/product/variant_controller.dart';
|
import '../../controllers/product/variant_controller.dart';
|
||||||
import '../../domain/provider/varient/varient_pro.dart';
|
import '../../domain/provider/varient/varient_pro.dart';
|
||||||
import '../../modules/product/product.dart';
|
import '../../modules/product/product.dart';
|
||||||
import '../../widgets/text_widget.dart';
|
|
||||||
|
|
||||||
class ProductViewPage extends StatefulWidget {
|
class ProductViewPage extends StatefulWidget {
|
||||||
final Product product;
|
final Product product;
|
||||||
|
|||||||
878
lib/view/product/products_list.dart
Normal file
878
lib/view/product/products_list.dart
Normal file
@@ -0,0 +1,878 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:nearledaily/view/product/product_details.dart';
|
||||||
|
import 'package:nearledaily/view/product/refill_home_screen.dart';
|
||||||
|
import 'package:wheel_chooser/wheel_chooser.dart';
|
||||||
|
|
||||||
|
import '../../widgets/fav_button.dart';
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// MODEL
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ProductItem {
|
||||||
|
final String name;
|
||||||
|
final String category;
|
||||||
|
final String price;
|
||||||
|
final String originalPrice;
|
||||||
|
final String imageUrl;
|
||||||
|
final Color bgColor;
|
||||||
|
final double rating;
|
||||||
|
final int reviewCount;
|
||||||
|
final String tag;
|
||||||
|
|
||||||
|
ProductItem({
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.price,
|
||||||
|
required this.originalPrice,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.bgColor,
|
||||||
|
required this.rating,
|
||||||
|
required this.reviewCount,
|
||||||
|
required this.tag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// SAMPLE DATA
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
final allProducts = [
|
||||||
|
ProductItem(
|
||||||
|
name: 'Margherita Pizza',
|
||||||
|
category: 'Dominos · 20 min',
|
||||||
|
price: '₹149',
|
||||||
|
originalPrice: '₹199',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500',
|
||||||
|
bgColor: const Color(0xFFFFF3E0),
|
||||||
|
rating: 4.6,
|
||||||
|
reviewCount: 512,
|
||||||
|
tag: 'Food',
|
||||||
|
),
|
||||||
|
|
||||||
|
ProductItem(
|
||||||
|
name: 'Fresh Veggie Basket',
|
||||||
|
category: 'BigBasket · 45 min',
|
||||||
|
price: '₹299',
|
||||||
|
originalPrice: '₹420',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1542838132-92c53300491e?w=500',
|
||||||
|
bgColor: const Color(0xFFE8F5E9),
|
||||||
|
rating: 4.5,
|
||||||
|
reviewCount: 180,
|
||||||
|
tag: 'Grocery',
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Margherita Pizza',
|
||||||
|
category: 'Dominos · 20 min',
|
||||||
|
price: '₹149',
|
||||||
|
originalPrice: '₹199',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500',
|
||||||
|
bgColor: const Color(0xFFFFF3E0),
|
||||||
|
rating: 4.6,
|
||||||
|
reviewCount: 512,
|
||||||
|
tag: 'Food',
|
||||||
|
),
|
||||||
|
|
||||||
|
ProductItem(
|
||||||
|
name: 'Chicken Biryani',
|
||||||
|
category: 'HotPot · 30 min',
|
||||||
|
price: '₹189',
|
||||||
|
originalPrice: '₹240',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1631515243349-e0cb75fb8d3a?w=500',
|
||||||
|
bgColor: const Color(0xFFFBE9E7),
|
||||||
|
rating: 4.8,
|
||||||
|
reviewCount: 320,
|
||||||
|
tag: 'Food',
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Margherita Pizza',
|
||||||
|
category: 'Dominos · 20 min',
|
||||||
|
price: '₹149',
|
||||||
|
originalPrice: '₹199',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500',
|
||||||
|
bgColor: const Color(0xFFFFF3E0),
|
||||||
|
rating: 4.6,
|
||||||
|
reviewCount: 512,
|
||||||
|
tag: 'Food',
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Birthday Cake',
|
||||||
|
category: 'CakeZone · 60 min',
|
||||||
|
price: '₹549',
|
||||||
|
originalPrice: '₹699',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=500',
|
||||||
|
bgColor: const Color(0xFFFCE4EC),
|
||||||
|
rating: 4.9,
|
||||||
|
reviewCount: 94,
|
||||||
|
tag: 'Bakery',
|
||||||
|
),
|
||||||
|
ProductItem(
|
||||||
|
name: 'Margherita Pizza',
|
||||||
|
category: 'Dominos · 20 min',
|
||||||
|
price: '₹149',
|
||||||
|
originalPrice: '₹199',
|
||||||
|
imageUrl:
|
||||||
|
'https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500',
|
||||||
|
bgColor: const Color(0xFFFFF3E0),
|
||||||
|
rating: 4.6,
|
||||||
|
reviewCount: 512,
|
||||||
|
tag: 'Food',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// FILTER CHIPS DATA
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const filterTabs = ['All', 'Food', 'Grocery', 'Bakery', 'Drinks'];
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// PRODUCTS PAGE
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ProductsList extends StatefulWidget {
|
||||||
|
const ProductsList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProductsList> createState() => _ProductsListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductsListState extends State<ProductsList> {
|
||||||
|
String _selectedFilter = 'All';
|
||||||
|
String _sortBy = 'Popular';
|
||||||
|
|
||||||
|
List<ProductItem> get _filtered => _selectedFilter == 'All'
|
||||||
|
? allProducts
|
||||||
|
: allProducts.where((p) => p.tag == _selectedFilter).toList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF7F7FA),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
|
||||||
|
// ── App Bar ──
|
||||||
|
SliverAppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
pinned: true,
|
||||||
|
title: const Text(
|
||||||
|
'Products',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
leading: const BackButton(color: Color(0xFF1A1A1A)),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search_rounded,
|
||||||
|
color: Color(0xFF1A1A1A)),
|
||||||
|
onPressed: () {
|
||||||
|
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => const AutoRefillDemo()));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.tune_rounded,
|
||||||
|
color: Color(0xFF1A1A1A)),
|
||||||
|
onPressed: () => _showSortSheet(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: const Color(0xFFEEEEEE),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Filter chips ──
|
||||||
|
SliverPersistentHeader(
|
||||||
|
pinned: true,
|
||||||
|
delegate: _FilterHeaderDelegate(
|
||||||
|
selectedFilter: _selectedFilter,
|
||||||
|
sortBy: _sortBy,
|
||||||
|
onFilterChanged: (f) => setState(() => _selectedFilter = f),
|
||||||
|
onSortTap: () => _showSortSheet(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ── Product grid ──
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 0.72,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => _ProductCard(item: _filtered[index]),
|
||||||
|
childCount: _filtered.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSortSheet(BuildContext context) {
|
||||||
|
final options = ['Popular', 'Price: Low to High', 'Price: High to Low', 'Rating', 'Discount'];
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
builder: (_) => Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE0E0E0),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Sort by',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...options.map((opt) => InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _sortBy = opt);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
opt,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: _sortBy == opt
|
||||||
|
? FontWeight.w700
|
||||||
|
: FontWeight.w400,
|
||||||
|
color: _sortBy == opt
|
||||||
|
? const Color(0xFF7C3AED)
|
||||||
|
: const Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_sortBy == opt)
|
||||||
|
const Icon(Icons.check_circle_rounded,
|
||||||
|
color: Color(0xFF7C3AED), size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// STICKY FILTER HEADER DELEGATE
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _FilterHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
final String selectedFilter;
|
||||||
|
final String sortBy;
|
||||||
|
final ValueChanged<String> onFilterChanged;
|
||||||
|
final VoidCallback onSortTap;
|
||||||
|
|
||||||
|
_FilterHeaderDelegate({
|
||||||
|
required this.selectedFilter,
|
||||||
|
required this.sortBy,
|
||||||
|
required this.onFilterChanged,
|
||||||
|
required this.onSortTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minExtent => 56;
|
||||||
|
@override
|
||||||
|
double get maxExtent => 56;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.white,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: filterTabs.length + 1, // +1 for sort chip
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
// Last item = Sort chip
|
||||||
|
if (index == filterTabs.length) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onSortTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF3F0FF),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFD4C5FF), width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.swap_vert_rounded,
|
||||||
|
size: 15, color: Color(0xFF662582)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
sortBy == 'Popular' ? 'Sort' : sortBy,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final tab = filterTabs[index];
|
||||||
|
final isSelected = selectedFilter == tab;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onFilterChanged(tab),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF662582)
|
||||||
|
: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF662582)
|
||||||
|
: const Color(0xFFE0E0E0),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
tab,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: const Color(0xFF757575),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: 1, color: const Color(0xFFEEEEEE)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRebuild(_FilterHeaderDelegate old) =>
|
||||||
|
old.selectedFilter != selectedFilter || old.sortBy != sortBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// PRODUCT CARD
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _ProductCard extends StatefulWidget {
|
||||||
|
final ProductItem item;
|
||||||
|
const _ProductCard({required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ProductCard> createState() => _ProductCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductCardState extends State<_ProductCard> {
|
||||||
|
bool _isFav = false;
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final item = widget.item;
|
||||||
|
int selectedIndex = 2;
|
||||||
|
|
||||||
|
final original = double.tryParse(
|
||||||
|
item.originalPrice.replaceAll(RegExp(r'[^\d.]'), '')) ??
|
||||||
|
0;
|
||||||
|
final current =
|
||||||
|
double.tryParse(item.price.replaceAll(RegExp(r'[^\d.]'), '')) ?? 0;
|
||||||
|
final discount =
|
||||||
|
original > 0 ? ((original - current) / original * 100).round() : 0;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// ── Image area ──
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// Image or emoji fallback
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => showProductSheet(context, item),
|
||||||
|
|
||||||
|
child: SizedBox(
|
||||||
|
height: 140,
|
||||||
|
width: double.infinity,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: item.imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => _fallback(item),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Discount badge — top left
|
||||||
|
if (discount > 0)
|
||||||
|
Positioned(
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF662582),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$discount% OFF',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Favourite — top right
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: FavButton(
|
||||||
|
size: 32,
|
||||||
|
initialValue: _isFav,
|
||||||
|
onChanged: (val) => setState(() => _isFav = val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Add button — bottom right ON image
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
right: 10,
|
||||||
|
child: GestureDetector(
|
||||||
|
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setModalState) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: 420,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
const Text(
|
||||||
|
"Choose Variant",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Scroll to select your size",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: PageController(
|
||||||
|
viewportFraction: 0.30,
|
||||||
|
initialPage: selectedIndex,
|
||||||
|
),
|
||||||
|
onPageChanged: (index) {
|
||||||
|
setModalState(() {
|
||||||
|
selectedIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
itemCount: 5,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final items = [
|
||||||
|
{
|
||||||
|
"name": "Small",
|
||||||
|
"price": "₹149",
|
||||||
|
"image":
|
||||||
|
"https://images.unsplash.com/photo-1513104890138-7c749659a591?w=500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Medium",
|
||||||
|
"price": "₹199",
|
||||||
|
"image":
|
||||||
|
"https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Large",
|
||||||
|
"price": "₹249",
|
||||||
|
"image":
|
||||||
|
"https://images.unsplash.com/photo-1514326640560-7d063ef2aed5?w=500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "XL",
|
||||||
|
"price": "₹299",
|
||||||
|
"image":
|
||||||
|
"https://images.unsplash.com/photo-1594007654729-407eedc4be65?w=500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Party",
|
||||||
|
"price": "₹399",
|
||||||
|
"image":
|
||||||
|
"https://images.unsplash.com/photo-1514326640560-7d063ef2aed5?w=500",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
final isSelected = selectedIndex == index;
|
||||||
|
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
transform: Matrix4.identity()
|
||||||
|
..scale(isSelected ? 1.15 : 1.15),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
width: isSelected ? 100 : 85,
|
||||||
|
height: isSelected ? 100 : 85,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF662582)
|
||||||
|
: Colors.transparent,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
|
||||||
|
),
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.network(
|
||||||
|
items[index]["image"]!,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
AnimatedDefaultTextStyle(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: isSelected ? 18 : 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.black
|
||||||
|
: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
child: Text(items[index]["name"]!),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
|
||||||
|
AnimatedDefaultTextStyle(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
style: TextStyle(
|
||||||
|
color: const Color(0xFF662582),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: isSelected ? 16 : 12,
|
||||||
|
),
|
||||||
|
child: Text(items[index]["price"]!),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF662582),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text(
|
||||||
|
"Confirm Selection",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.12),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Info area ──
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
// Category
|
||||||
|
Text(
|
||||||
|
item.category,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: Color(0xFF662582),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
item.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Rating
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.star_rounded,
|
||||||
|
size: 13, color: Color(0xFFFFC107)),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(
|
||||||
|
'${item.rating}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
Text(
|
||||||
|
'(${item.reviewCount})',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: Color(0xFF9E9E9E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Price
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.price,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
if (discount > 0)
|
||||||
|
Text(
|
||||||
|
item.originalPrice,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Color(0xFF9E9E9E),
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _fallback(ProductItem item) {
|
||||||
|
return Container(
|
||||||
|
height: 140,
|
||||||
|
width: double.infinity,
|
||||||
|
color: item.bgColor,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.image_not_supported,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1228
lib/view/product/refill_home_screen.dart
Normal file
1228
lib/view/product/refill_home_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import 'package:lottie/lottie.dart';
|
|||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
import '../../controllers/tenant/create_tenant.dart';
|
import '../../controllers/tenant/create_tenant.dart';
|
||||||
import '../../controllers/tenant_controller /tenant_list.dart';
|
import '../../controllers/tenant_list/tenant_controller.dart';
|
||||||
import '../home_view.dart';
|
import '../home_view.dart';
|
||||||
|
|
||||||
class QrScannerPage extends StatefulWidget {
|
class QrScannerPage extends StatefulWidget {
|
||||||
|
|||||||
118
lib/widgets/fav_button.dart
Normal file
118
lib/widgets/fav_button.dart
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FavButton extends StatefulWidget {
|
||||||
|
final bool initialValue;
|
||||||
|
final ValueChanged<bool>? onChanged;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const FavButton({
|
||||||
|
super.key,
|
||||||
|
this.initialValue = false,
|
||||||
|
this.onChanged,
|
||||||
|
this.size = 40,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FavButton> createState() => _FavButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavButtonState extends State<FavButton>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late bool _isFav;
|
||||||
|
late AnimationController _ctrl;
|
||||||
|
late Animation<double> _scale;
|
||||||
|
late Animation<double> _particleProgress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_isFav = widget.initialValue;
|
||||||
|
_ctrl = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 450),
|
||||||
|
);
|
||||||
|
_scale = TweenSequence([
|
||||||
|
TweenSequenceItem(
|
||||||
|
tween: Tween(begin: 1.0, end: 1.4)
|
||||||
|
.chain(CurveTween(curve: Curves.easeOut)),
|
||||||
|
weight: 40,
|
||||||
|
),
|
||||||
|
TweenSequenceItem(
|
||||||
|
tween: Tween(begin: 1.4, end: 1.0)
|
||||||
|
.chain(CurveTween(curve: Curves.elasticOut)),
|
||||||
|
weight: 60,
|
||||||
|
),
|
||||||
|
]).animate(_ctrl);
|
||||||
|
_particleProgress =
|
||||||
|
CurvedAnimation(parent: _ctrl, curve: Curves.easeOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggle() {
|
||||||
|
setState(() => _isFav = !_isFav);
|
||||||
|
widget.onChanged?.call(_isFav);
|
||||||
|
if (_isFav) _ctrl.forward(from: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _toggle,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _ctrl,
|
||||||
|
builder: (context, child) => CustomPaint(
|
||||||
|
painter: _isFav ? _BurstPainter(_particleProgress.value) : null,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: _isFav ? _scale.value : 1.0,
|
||||||
|
child: Container(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _isFav
|
||||||
|
? Colors.red.withOpacity(0.1)
|
||||||
|
: Colors.transparent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_isFav
|
||||||
|
? Icons.favorite_rounded
|
||||||
|
: Icons.favorite_border_rounded,
|
||||||
|
size: widget.size * 0.5,
|
||||||
|
color: _isFav ? Colors.redAccent : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BurstPainter extends CustomPainter {
|
||||||
|
final double progress;
|
||||||
|
_BurstPainter(this.progress);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (progress == 0) return;
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.redAccent.withOpacity(1 - progress);
|
||||||
|
const count = 7;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
final angle = (i / count) * 2 * pi;
|
||||||
|
final dist = progress * 24;
|
||||||
|
final pos = center + Offset(dist * cos(angle), dist * sin(angle));
|
||||||
|
canvas.drawCircle(pos, 3 * (1 - progress * 0.5), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_BurstPainter old) => old.progress != progress;
|
||||||
|
}
|
||||||
107
lib/widgets/grocery_card.dart
Normal file
107
lib/widgets/grocery_card.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../modules/search/grocery_item.dart';
|
||||||
|
|
||||||
|
class GroceryCard extends StatelessWidget {
|
||||||
|
final GroceryItem item;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const GroceryCard({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(item.emoji, style: const TextStyle(fontSize: 30)),
|
||||||
|
Text(item.name),
|
||||||
|
Text(item.quantity),
|
||||||
|
Text(item.inCart ? "Added" : "Add"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoreCard extends StatelessWidget {
|
||||||
|
final dynamic storeData;
|
||||||
|
|
||||||
|
const StoreCard({
|
||||||
|
super.key,
|
||||||
|
required this.storeData,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final store = storeData["store"];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
bottom: 15,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
store["name"],
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
Text(store["address"]),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"₹${store["total_discounted_price"]}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color: Colors.green,
|
||||||
|
fontWeight:
|
||||||
|
FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text(
|
||||||
|
"Select Store",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Save ₹${store["total_savings"]}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
383
lib/widgets/search_widgets.dart
Normal file
383
lib/widgets/search_widgets.dart
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../modules/search/grocery_item.dart';
|
||||||
|
|
||||||
|
// ─── Grocery Card ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class GroceryCard extends StatelessWidget {
|
||||||
|
final GroceryItem item;
|
||||||
|
final VoidCallback onToggle;
|
||||||
|
|
||||||
|
const GroceryCard({super.key, required this.item, required this.onToggle});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.inCart ? const Color(0xFFF8FDF9) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: item.inCart ? const Color(0xFF1A6B3C) : const Color(0xFFE2E8E0),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 14, 12, 12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(item.emoji, style: const TextStyle(fontSize: 36)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
item.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF1A1F1C),
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF3E8),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item.quantity,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFFF97316),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onToggle,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.inCart
|
||||||
|
? const Color(0xFF1A6B3C)
|
||||||
|
: const Color(0xFFE8F5EE),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
item.inCart ? '✓ Added' : '+ Add to cart',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: item.inCart ? Colors.white : const Color(0xFF1A6B3C),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (item.inCart)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF1A6B3C),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.check_rounded, color: Colors.white, size: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Skeleton Card ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class SkeletonCard extends StatefulWidget {
|
||||||
|
const SkeletonCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SkeletonCard> createState() => _SkeletonCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SkeletonCardState extends State<SkeletonCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _ctrl;
|
||||||
|
late Animation<double> _anim;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = AnimationController(
|
||||||
|
vsync: this, duration: const Duration(milliseconds: 900))
|
||||||
|
..repeat(reverse: true);
|
||||||
|
_anim = Tween<double>(begin: 0.4, end: 1.0).animate(_ctrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: _anim,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: const Color(0xFFE2E8E0), width: 1.5),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 14, 12, 12),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFE8EDE9),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_skLine(0.8),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_skLine(0.6),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE8EDE9),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _skLine(double widthFraction) {
|
||||||
|
return FractionallySizedBox(
|
||||||
|
widthFactor: widthFraction,
|
||||||
|
child: Container(
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE8EDE9),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chip ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class SearchChip extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool muted;
|
||||||
|
|
||||||
|
const SearchChip({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
this.muted = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: muted ? const Color(0xFFEDF2F0) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: muted ? Colors.transparent : const Color(0xFFE2E8E0),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: muted ? 12 : 13,
|
||||||
|
color: muted ? const Color(0xFF6B7A72) : const Color(0xFF1A1F1C),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Cart Bottom Sheet ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class CartSheet extends StatelessWidget {
|
||||||
|
final List<GroceryItem> items;
|
||||||
|
final void Function(GroceryItem) onRemove;
|
||||||
|
|
||||||
|
const CartSheet({super.key, required this.items, required this.onRemove});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE2E8E0),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Your Cart',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1A1F1C)),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFEDF2F0),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${items.length} items',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6B7A72)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.45,
|
||||||
|
),
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: items.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final item = items[i];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(item.emoji, style: const TextStyle(fontSize: 28)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(item.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF1A1F1C))),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(item.quantity,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFFF97316),
|
||||||
|
fontWeight: FontWeight.w500)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline_rounded,
|
||||||
|
color: Color(0xFFE24B4A), size: 20),
|
||||||
|
onPressed: () => onRemove(item),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('🛒 Order placed successfully!'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Color(0xFF1A6B3C),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF1A6B3C),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14)),
|
||||||
|
textStyle:
|
||||||
|
const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
child: const Text('Place Order'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Section Label ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class SectionLabel extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const SectionLabel(this.label, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
label.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6B7A72),
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,16 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <gtk/gtk_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
gtk
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import app_links
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
|
import firebase_analytics
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
@@ -20,14 +22,17 @@ import package_info_plus
|
|||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import speech_to_text
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import webview_flutter_wkwebview
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin"))
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
@@ -40,6 +45,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
|
|||||||
262
pubspec.lock
262
pubspec.lock
@@ -49,6 +49,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
app_links:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: app_links
|
||||||
|
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.4.1"
|
||||||
|
app_links_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_linux
|
||||||
|
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
app_links_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_platform_interface
|
||||||
|
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
app_links_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: app_links_web
|
||||||
|
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -89,6 +121,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.3"
|
version: "1.2.3"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
|
camera:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: camera
|
||||||
|
sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.0+1"
|
||||||
|
camera_android_camerax:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_android_camerax
|
||||||
|
sha256: "50c3fd81228826635af4875330febb193c74a2e12a4d19c1b95e2a67f0017bb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.3"
|
||||||
|
camera_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_avfoundation
|
||||||
|
sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
|
camera_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_platform_interface
|
||||||
|
sha256: "7ac852d77699acee79f0d438b793feee26721841e50973576419ff5c6d95e9b7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
camera_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: camera_web
|
||||||
|
sha256: "1245a480a113437f8d46d19c0fb90cea9db921436d9cf2ba5fb11854a1312693"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+4"
|
||||||
|
card_animation_hover:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: card_animation_hover
|
||||||
|
sha256: fab2ff2268dcb2ed9e9cd1803ac752911a1f930892fe36a9363b3dc9c6214c90
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
carousel_slider:
|
carousel_slider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -101,10 +205,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -321,6 +425,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+4"
|
version: "0.9.3+4"
|
||||||
|
firebase_analytics:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_analytics
|
||||||
|
sha256: "4f85b161772e1d54a66893ef131c0a44bd9e552efa78b33d5f4f60d2caa5c8a3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.6.0"
|
||||||
|
firebase_analytics_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_analytics_platform_interface
|
||||||
|
sha256: a44b6d1155ed5cae7641e3de7163111cfd9f6f6c954ca916dc6a3bdfa86bf845
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.4.3"
|
||||||
|
firebase_analytics_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_analytics_web
|
||||||
|
sha256: c7d1ed1f86ae64215757518af5576ff88341c8ce5741988c05cc3b2e07b0b273
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.10+16"
|
||||||
firebase_auth:
|
firebase_auth:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -422,14 +550,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
version: "3.4.1"
|
||||||
flutter_contacts:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_contacts
|
|
||||||
sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.9+2"
|
|
||||||
flutter_floating_bottom_bar:
|
flutter_floating_bottom_bar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -688,6 +808,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.14+2"
|
version: "0.5.14+2"
|
||||||
|
google_mlkit_commons:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_mlkit_commons
|
||||||
|
sha256: "9990a65f407a3ef6bae646bf10143faa93fec126683771465bc6c0b43fb0e6e9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.1"
|
||||||
|
google_mlkit_object_detection:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_mlkit_object_detection
|
||||||
|
sha256: c516ed16c011a47c3abc8f087c6183fa1980b032278a4a1b45872e98d6f4180e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
|
google_mlkit_text_recognition:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_mlkit_text_recognition
|
||||||
|
sha256: "179349417066fa2c275d7a6ed6cbceeb7fa265d73aacdb2d732f1a2991face0a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
google_places_flutter:
|
google_places_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -696,6 +840,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
gtk:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: gtk
|
||||||
|
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
home_widget:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: home_widget
|
||||||
|
sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -817,13 +977,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.5"
|
version: "4.2.5"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.3"
|
||||||
intro_slider:
|
intro_slider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -916,26 +1076,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -992,6 +1152,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
otp_timer_button:
|
otp_timer_button:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1152,6 +1320,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "3.1.6"
|
||||||
|
play_install_referrer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: play_install_referrer
|
||||||
|
sha256: "7dd808236d35d15199d5e7a5b55488e4343c1b67c4694902d6b95196b22b19cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1301,6 +1477,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
sleek_circular_slider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sleek_circular_slider
|
||||||
|
sha256: "89c7d0d37befc9818a2b53ef6ff126c43b70485dabb189b734d46106db190931"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
sliver_tools:
|
sliver_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1333,6 +1517,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
speech_to_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: speech_to_text
|
||||||
|
sha256: "75587f7400f485fdf166beacd471549d98fe5d58e634f708916bb65dec05d6a4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.4.0"
|
||||||
|
speech_to_text_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: speech_to_text_platform_interface
|
||||||
|
sha256: a7e16e02853853ed7534ac2bde9a1c4f39c8879970a7974ac6ff832d4bdaa4b0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
speech_to_text_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: speech_to_text_windows
|
||||||
|
sha256: "2d1d10565b23262386b453b33656299608dc7a66784453735d6c1318f13f44d7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1433,10 +1641,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.11"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1589,6 +1797,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.23.0"
|
version: "3.23.0"
|
||||||
|
wheel_chooser:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: wheel_chooser
|
||||||
|
sha256: "3fee36f081f321c58a0b7b4afcdd92599f2ca520b3a1420084774e6b19cca1d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1630,5 +1846,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: ">=3.32.0"
|
flutter: ">=3.44.0"
|
||||||
|
|||||||
24
pubspec.yaml
24
pubspec.yaml
@@ -3,7 +3,7 @@ description: "Nearle Neighbourhood Living Platform"
|
|||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.6.5+258
|
version: 1.6.8+261
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
@@ -27,7 +27,6 @@ dependencies:
|
|||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
readmore: ^3.0.0
|
readmore: ^3.0.0
|
||||||
animated_text_kit: ^4.3.0
|
animated_text_kit: ^4.3.0
|
||||||
flutter_contacts: ^1.1.9
|
|
||||||
share_plus: ^10.0.2
|
share_plus: ^10.0.2
|
||||||
rounded_loading_button_plus: ^3.0.1
|
rounded_loading_button_plus: ^3.0.1
|
||||||
sms_autofill: ^2.4.1
|
sms_autofill: ^2.4.1
|
||||||
@@ -74,6 +73,26 @@ dependencies:
|
|||||||
flutter_speed_dial: ^7.0.0
|
flutter_speed_dial: ^7.0.0
|
||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
razorpay_flutter: ^1.3.6
|
razorpay_flutter: ^1.3.6
|
||||||
|
play_install_referrer: ^0.5.0
|
||||||
|
app_links: ^6.3.3
|
||||||
|
firebase_analytics: ^11.6.0
|
||||||
|
speech_to_text: ^7.0.0
|
||||||
|
cached_network_image: ^3.3.1
|
||||||
|
wheel_chooser: ^1.1.2
|
||||||
|
card_animation_hover: ^0.0.1
|
||||||
|
intl: ^0.20.3
|
||||||
|
sleek_circular_slider: ^2.1.0
|
||||||
|
camera: ^0.12.0+1
|
||||||
|
google_mlkit_object_detection: ^0.13.0
|
||||||
|
google_mlkit_text_recognition: ^0.13.0
|
||||||
|
home_widget: ^0.6.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -110,6 +129,7 @@ flutter:
|
|||||||
- assets/lotties/
|
- assets/lotties/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Proxima Nova
|
- family: Proxima Nova
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
||||||
@@ -13,9 +14,12 @@
|
|||||||
#include <geolocator_windows/geolocator_windows.h>
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
|
#include <speech_to_text_windows/speech_to_text_windows.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
@@ -30,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
|
SpeechToTextWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SpeechToTextWindows"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
app_links
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
firebase_auth
|
firebase_auth
|
||||||
@@ -10,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
geolocator_windows
|
geolocator_windows
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
share_plus
|
share_plus
|
||||||
|
speech_to_text_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user