commit 39650469843e3f4fab8c87d5aa2dea840fa8b73d Author: Suriya Date: Fri Jun 26 13:43:37 2026 +0530 Initial commit of Doormile CRM Mobile with UI modernizations diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..f742fe2 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: android + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: ios + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: linux + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: macos + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: web + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + - platform: windows + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..063cfcb --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# doormile_flutter + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + + +PS E:\Doormile\Doormile-app\crm_flutter> claude + +Resume this session with: +claude --resume 0ea33e07-ef7d-4b60-99ee-23d954cb58ac \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..79e842a --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,75 @@ +import java.util.Properties +import java.io.FileInputStream + +val keystoreProperties = Properties() +val keystorePropertiesFile = File(rootDir, "key.properties") + +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +plugins { + id("com.android.application") + id("kotlin-android") + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.doormile.crm" + + compileSdk = flutter.compileSdkVersion + ndkVersion = "28.2.13676358" + + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + applicationId = "com.doormile.crm" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + + isMinifyEnabled = true + isShrinkResources = true + ndk { + debugSymbolLevel = "SYMBOL_TABLE" + } + } + } + + packaging { + jniLibs { + excludes += setOf("**/libjsc.so") + useLegacyPackaging = true + } + + resources { + excludes += setOf("META-INF/*") + } + } +} + +flutter { + source = "../.." +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fc30b8a --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/doormile/crm/MainActivity.kt b/android/app/src/main/kotlin/com/doormile/crm/MainActivity.kt new file mode 100644 index 0000000..d6de25b --- /dev/null +++ b/android/app/src/main/kotlin/com/doormile/crm/MainActivity.kt @@ -0,0 +1,5 @@ +package com.doormile.crm + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c2ed520 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5e96803 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..d52e483 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5857463 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9d1ea92 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d7a782d Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..4f1ae62 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml new file mode 100644 index 0000000..5f349f7 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..299cab7 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..6f66668 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..6c3e7dc Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..e67ce0f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..c4261d2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..a1b475a --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..b7ecb1f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=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 +kotlin.incremental=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/api.txt b/api.txt new file mode 100644 index 0000000..859a0c7 --- /dev/null +++ b/api.txt @@ -0,0 +1,252 @@ + +like how we are doing thi specifer using mobile or wb + + +To specify whether the survey was taken on mobile or web, you just need to hardcode the registration_source identifier directly into the API request code of your respective frontends. + +This is the most reliable way to handle it, because your mobile codebase uniquely knows it's the mobile app, and your web codebase uniquely knows it's the web app. + +Here is exactly how you handle it in both environments: + +1. In your Flutter (Mobile) App +When you build the JSON body to send your POST request via the http package or dio, you simply inject "mobile" and grab the current logged-in user's ID from your local state: + +dart +// Inside your Flutter API service +Future submitSurvey(String firstName, double lat, double lon, int currentUserId) async { + final payload = { + "first_name": firstName, + // ... other fields + "survey_lat": lat, + "survey_long": lon, + + // Explicitly tag this as coming from the mobile app + "registration_source": "mobile", + "registered_by_id": currentUserId + }; + await http.post( + Uri.parse('https://api.doormile.com/api/v1/crm/clients'), + headers: {"Content-Type": "application/json"}, + body: jsonEncode(payload), + ); +} +2. In your React / Web App +Similarly, inside your Web frontend's API submission function, you explicitly hardcode "web" as the source: + +javascript +// Inside your Web frontend API utility +async function submitSurvey(formData, lat, lon, currentUserId) { + const payload = { + first_name: formData.firstName, + // ... other fields + survey_lat: lat, + survey_long: lon, + + // Explicitly tag this as coming from the web dashboard + registration_source: "web", + registered_by_id: currentUserId + }; + await fetch('https://api.doormile.com/api/v1/crm/clients', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); +} +Alternative Option: Backend "User-Agent" Detection +If you don't want to pass it manually in the JSON payload, we can modify the Golang backend to automatically detect the platform by reading the HTTP Headers. + +When a request hits your API, Fiber can check the User-Agent header (Flutter usually sends Dart/ (dart:io), while Web sends standard browser data like Mozilla/5.0...). Let me know if you would prefer the backend to automate this detection instead! + +12:16 PM + + +Here are the **GET** and **POST** APIs that are now fully functional and mapped in your backend. + +All endpoints use the base URL path: `http://localhost:8081/api/v1/crm/clients` + +### 1. Create a New Client (POST) +**Endpoint:** `POST /api/v1/crm/clients` +**Description:** Registers a new client in `doormile_clients`, hashes their password, sets up their auth credentials in `doormile_auth`, and optionally accepts a vector for future AI search. + +**Request Body (JSON):** +```json +{ + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "password": "securepassword123", + "phone": "+1234567890", + "address": "123 Main St, Tech City", + "vector": [0.1, 0.5, -0.3] // Optional: For Qdrant AI embedding integration +} +``` + +**Success Response (201 Created):** +```json +{ + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "address": "123 Main St, Tech City", + "role": "user" +} +``` + +--- + +### 2. Fetch All Clients (GET) +**Endpoint:** `GET /api/v1/crm/clients` +**Description:** Returns a list of all registered clients alongside their authentication details (like email and role) by performing a SQL join behind the scenes. + +**Request Body:** None + +**Success Response (200 OK):** +```json +[ + { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "address": "123 Main St, Tech City", + "role": "user" + }, + { + "id": 2, + "first_name": "Jane", + "last_name": "Smith", + "email": "jane@example.com", + "phone": "+1987654321", + "address": "456 Commerce Rd", + "role": "user" + } +] +``` + +--- + +### 3. Fetch a Specific Client (GET) +**Endpoint:** `GET /api/v1/crm/clients/:id` +**Description:** Returns the details for a specific client matching the provided ID in the URL. + +**Request Body:** None + +**Success Response (200 OK) for `/api/v1/crm/clients/1`:** +```json +{ + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "address": "123 Main St, Tech City", + "role": "user" +} +``` + +### Implementation Notes: +* You can find the exact backend logic for these in `controllers/clientController.go`. +* The structured structs representing these API JSON payloads are located in `dto/client.go`. + +Edited client.go +Edited client.go +Edited clientController.go +Ran command: `& "C:\Program Files\Go\bin\go.exe" run main.go` + +This makes perfect sense—having an exact audit trail of exactly *where*, *when*, and *by whom* the survey was captured (alongside whether it was captured in the field on `mobile` or from the office on `web`) is crucial for data integrity. + +I have completely refactored the models, DTOs, and API logic to rename the columns and add these tracking fields. Additionally, I successfully booted up the backend in the background to automatically migrate these changes into your active PostgreSQL database! + +Here is exactly what was updated: + +1. **Renamed GPS Columns**: `latitude` and `longitude` were renamed strictly to `surveylat` and `surveylong` across the database columns and the JSON payload schemas. +2. **Registration Origin Tracking**: Added a `registration_source` string field (so you can pass "mobile" or "web"). +3. **Creator Identifier**: Added a `registered_by_id` (Integer) field to uniquely track the specific logged-in user who submitted the survey payload. + +### Your Updated POST Payload Schema +Your `POST /api/v1/crm/clients` payload originating from both your Mobile Flutter App and your Web App should now look exactly like this: + +```json +{ + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "password": "securepassword123", + "phone": "+1234567890", + "address": "123 Main St, Tech City", + "survey_lat": 37.7749, + "survey_long": -122.4194, + "pincode": "94103", + "registration_source": "mobile", + "registered_by_id": 42 +} +``` + +Whenever you retrieve a client record using `GET /api/v1/crm/clients/:id`, it will return these exact fields so you'll always know the survey's origin platform, the exact location it was plotted, and the user who processed it! + + +To specify whether the survey was taken on **mobile** or **web**, you just need to hardcode the `registration_source` identifier directly into the API request code of your respective frontends. + +This is the most reliable way to handle it, because your mobile codebase uniquely knows it's the mobile app, and your web codebase uniquely knows it's the web app. + +Here is exactly how you handle it in both environments: + +### 1. In your Flutter (Mobile) App +When you build the JSON body to send your POST request via the `http` package or `dio`, you simply inject `"mobile"` and grab the current logged-in user's ID from your local state: + +```dart +// Inside your Flutter API service +Future submitSurvey(String firstName, double lat, double lon, int currentUserId) async { + final payload = { + "first_name": firstName, + // ... other fields + "survey_lat": lat, + "survey_long": lon, + + // Explicitly tag this as coming from the mobile app + "registration_source": "mobile", + "registered_by_id": currentUserId + }; + + await http.post( + Uri.parse('https://api.doormile.com/api/v1/crm/clients'), + headers: {"Content-Type": "application/json"}, + body: jsonEncode(payload), + ); +} +``` + +### 2. In your React / Web App +Similarly, inside your Web frontend's API submission function, you explicitly hardcode `"web"` as the source: + +```javascript +// Inside your Web frontend API utility +async function submitSurvey(formData, lat, lon, currentUserId) { + const payload = { + first_name: formData.firstName, + // ... other fields + survey_lat: lat, + survey_long: lon, + + // Explicitly tag this as coming from the web dashboard + registration_source: "web", + registered_by_id: currentUserId + }; + + await fetch('https://api.doormile.com/api/v1/crm/clients', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); +} +``` + +### Alternative Option: Backend "User-Agent" Detection +If you don't want to pass it manually in the JSON payload, we can modify the Golang backend to automatically detect the platform by reading the HTTP Headers. + +When a request hits your API, Fiber can check the `User-Agent` header (Flutter usually sends `Dart/ (dart:io)`, while Web sends standard browser data like `Mozilla/5.0...`). Let me know if you would prefer the backend to automate this detection instead! + + diff --git a/assets/_icon_preview.png b/assets/_icon_preview.png new file mode 100644 index 0000000..f643518 Binary files /dev/null and b/assets/_icon_preview.png differ diff --git a/assets/doormile_d_only.png b/assets/doormile_d_only.png new file mode 100644 index 0000000..2020742 Binary files /dev/null and b/assets/doormile_d_only.png differ diff --git a/assets/doormile_fg.png b/assets/doormile_fg.png new file mode 100644 index 0000000..85ab618 Binary files /dev/null and b/assets/doormile_fg.png differ diff --git a/assets/doormile_round_fg.png b/assets/doormile_round_fg.png new file mode 100644 index 0000000..fe08dc8 Binary files /dev/null and b/assets/doormile_round_fg.png differ diff --git a/assets/doormilelogo512512.png b/assets/doormilelogo512512.png new file mode 100644 index 0000000..9d31192 Binary files /dev/null and b/assets/doormilelogo512512.png differ diff --git a/assets/doormilelogo_full.png b/assets/doormilelogo_full.png new file mode 100644 index 0000000..2248dea Binary files /dev/null and b/assets/doormilelogo_full.png differ diff --git a/assets/doormilelogoround.png b/assets/doormilelogoround.png new file mode 100644 index 0000000..4d89c35 Binary files /dev/null and b/assets/doormilelogoround.png differ diff --git a/assets/preloader.png b/assets/preloader.png new file mode 100644 index 0000000..92cb60e Binary files /dev/null and b/assets/preloader.png differ diff --git a/assets/test_merge.png b/assets/test_merge.png new file mode 100644 index 0000000..d577929 Binary files /dev/null and b/assets/test_merge.png differ diff --git a/buildlog.txt b/buildlog.txt new file mode 100644 index 0000000..226e669 --- /dev/null +++ b/buildlog.txt @@ -0,0 +1,2551 @@ +[ +9 ms] executing: sysctl hw.optional.arm64 +[ +2 ms] Exit code 0 from: sysctl hw.optional.arm64 +[ ] hw.optional.arm64: 1 +[ +2 ms] Artifact Instance of 'MaterialFonts' is not required, skipping update. +[ ] Artifact Instance of 'GradleWrapper' is not required, skipping update. +[ ] Artifact Instance of 'AndroidGenSnapshotArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'AndroidInternalBuildArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IOSEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterWebSdk' is not required, skipping update. +[ ] Artifact Instance of 'LegacyCanvasKitRemover' is not required, skipping update. +[ ] Artifact Instance of 'FlutterSdk' is not required, skipping update. +[ ] Artifact Instance of 'WindowsEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'MacOSEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'LinuxEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'LinuxFuchsiaSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'MacOSFuchsiaSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterRunnerSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterRunnerDebugSymbols' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FontSubsetArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'PubDependencies' is not required, skipping update. +[ ] executing: [/Users/anbarasu/flutter/] git -c log.showSignature=false log HEAD -n 1 --pretty=format:%ad --date=iso +[ +6 ms] Exit code 0 from: git -c log.showSignature=false log HEAD -n 1 --pretty=format:%ad --date=iso +[ ] 2026-05-15 14:13:13 -0700 +[ +3 ms] Artifact Instance of 'AndroidGenSnapshotArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'AndroidInternalBuildArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IOSEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterWebSdk' is not required, skipping update. +[ ] Artifact Instance of 'FlutterEngineStamp' is not required, skipping update. +[ ] Artifact Instance of 'LegacyCanvasKitRemover' is not required, skipping update. +[ ] Artifact Instance of 'WindowsEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'MacOSEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'LinuxEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'LinuxFuchsiaSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'MacOSFuchsiaSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterRunnerSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterRunnerDebugSymbols' is not required, skipping update. +[ +12 ms] Artifact Instance of 'MaterialFonts' is not required, skipping update. +[ ] Artifact Instance of 'GradleWrapper' is not required, skipping update. +[ ] Artifact Instance of 'AndroidInternalBuildArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IOSEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterWebSdk' is not required, skipping update. +[ ] Artifact Instance of 'FlutterEngineStamp' is not required, skipping update. +[ ] Artifact Instance of 'LegacyCanvasKitRemover' is not required, skipping update. +[ ] Artifact Instance of 'FlutterSdk' is not required, skipping update. +[ ] Artifact Instance of 'WindowsEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'MacOSEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'LinuxEngineArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'LinuxFuchsiaSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'MacOSFuchsiaSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterRunnerSDKArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FlutterRunnerDebugSymbols' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'FontSubsetArtifacts' is not required, skipping update. +[ ] Artifact Instance of 'PubDependencies' is not required, skipping update. +[ +23 ms] Skipping pub get: version match. +[ +25 ms] Found plugin path_provider at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ +[ +1 ms] Found plugin path_provider_windows at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ +[ +1 ms] Found plugin path_provider_linux at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +[ +1 ms] Found plugin path_provider_foundation at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/ +[ +3 ms] Found plugin path_provider_android at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/ +[ ] Found plugin jni_flutter at /Users/anbarasu/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/ +[ ] Found plugin jni at /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/ +[ ] Found plugin geolocator at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator-13.0.4/ +[ ] Found plugin geolocator_windows at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/ +[ ] Found plugin geolocator_web at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/ +[ ] Found plugin geolocator_apple at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/ +[ ] Found plugin geolocator_android at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/ +[ +5 ms] executing: sysctl hw.optional.arm64 +[ +2 ms] Exit code 0 from: sysctl hw.optional.arm64 +[ ] hw.optional.arm64: 1 +[ ] executing: /usr/bin/arch -arm64e xcrun xcodebuild -version +[ +58 ms] Exit code 0 from: /usr/bin/arch -arm64e xcrun xcodebuild -version +[ ] Xcode 26.2 + Build version 17C52 +[ +16 ms] Found plugin path_provider at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ +[ ] Found plugin path_provider_windows at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ +[ ] Found plugin path_provider_linux at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +[ ] Found plugin path_provider_foundation at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/ +[ +1 ms] Found plugin path_provider_android at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/ +[ ] Found plugin jni_flutter at /Users/anbarasu/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/ +[ ] Found plugin jni at /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/ +[ ] Found plugin geolocator at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator-13.0.4/ +[ ] Found plugin geolocator_windows at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/ +[ ] Found plugin geolocator_web at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/ +[ ] Found plugin geolocator_apple at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/ +[ ] Found plugin geolocator_android at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/ +[ +4 ms] Skipping generating web_plugin_registrant.dart because it is up-to-date. +[ +9 ms] Found plugin path_provider at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider-2.1.5/ +[ ] Found plugin path_provider_windows at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ +[ ] Found plugin path_provider_linux at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +[ ] Found plugin path_provider_foundation at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/ +[ +2 ms] Found plugin path_provider_android at /Users/anbarasu/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/ +[ ] Found plugin jni_flutter at /Users/anbarasu/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/ +[ ] Found plugin jni at /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/ +[ ] Found plugin geolocator at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator-13.0.4/ +[ ] Found plugin geolocator_windows at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/ +[ ] Found plugin geolocator_web at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/ +[ ] Found plugin geolocator_apple at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/ +[ ] Found plugin geolocator_android at /Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/ +[ +2 ms] Generating /Users/anbarasu/Downloads/doormile_flutter/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +[ ] Skipping generating GeneratedPluginRegistrant.java because it is up-to-date. +[ +1 ms] Skipping generating generated_plugin_registrant.h because it is up-to-date. +[ ] Skipping generating generated_plugin_registrant.cc because it is up-to-date. +[ ] Skipping generating generated_plugins.cmake because it is up-to-date. +[ ] Skipping generating generated_plugin_registrant.h because it is up-to-date. +[ ] Skipping generating generated_plugin_registrant.cc because it is up-to-date. +[ ] Skipping generating generated_plugins.cmake because it is up-to-date. +[ ] Skipping generating GeneratedPluginRegistrant.h because it is up-to-date. +[ ] Skipping generating GeneratedPluginRegistrant.m because it is up-to-date. +[ ] Skipping generating GeneratedPluginRegistrant.swift because it is up-to-date. +[ +5 ms] executing: pgrep -n -f -l xcodebuild -clonedSourcePackagesDirPath /Users/anbarasu/Downloads/doormile_flutter/build/ios/SourcePackages -resolvePackageDependencies +[ +13 ms] executing: [/Users/anbarasu/Downloads/doormile_flutter/ios/] /usr/bin/arch -arm64e xcrun xcodebuild -clonedSourcePackagesDirPath /Users/anbarasu/Downloads/doormile_flutter/build/ios/SourcePackages -resolvePackageDependencies +[ +90 ms] executing: /usr/bin/plutil -convert xml1 -o - /Applications/Android Studio.app/Contents/Info.plist +[ +5 ms] Exit code 0 from: /usr/bin/plutil -convert xml1 -o - /Applications/Android Studio.app/Contents/Info.plist +[ ] + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + ipr + + CFBundleTypeIconFile + studio.icns + CFBundleTypeName + Android Studio Project File + CFBundleTypeRole + Editor + + + CFBundleTypeExtensions + + * + + CFBundleTypeName + All documents + CFBundleTypeOSTypes + + **** + + CFBundleTypeRole + Editor + LSTypeIsPackage + + + + CFBundleExecutable + studio + CFBundleGetInfoString + Android Studio 2025.2, build AI-252.25557.131.2521.14432022. Copyright JetBrains s.r.o., (c) 2000-2025 + CFBundleIconFile + studio.icns + CFBundleIdentifier + com.google.android.studio + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Android Studio + CFBundlePackageType + APPL + CFBundleShortVersionString + 2025.2 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + Stacktrace + CFBundleURLSchemes + + idea + + + + CFBundleVersion + AI-252.25557.131.2521.14432022 + LSApplicationCategoryType + public.app-category.developer-tools + LSArchitecturePriority + + arm64 + + LSMinimumSystemVersion + 10.13 + LSRequiresNativeExecution + YES + NSAppleEventsUsageDescription + An application in Android Studio requests the ability to send Apple events. + NSBluetoothAlwaysUsageDescription + An application in Android Studio requests access to Bluetooth. + NSCameraUsageDescription + An application in Android Studio requests access to the device's camera. + NSDesktopFolderUsageDescription + An application in Android Studio requests access to the user's Desktop folder. + NSDocumentsFolderUsageDescription + An application in Android Studio requests access to the user's Documents folder. + NSDownloadsFolderUsageDescription + An application in Android Studio requests access to the user's Downloads folder. + NSHighResolutionCapable + + NSLocalNetworkUsageDescription + Android Studio's features, such as SSH and database connectivity, require permissions for accessing the local network and sending broadcast requests. + NSLocationUsageDescription + An application in Android Studio requests access to the user's location information. + NSMicrophoneUsageDescription + An application in Android Studio requests access to the device's microphone. + NSNetworkVolumesUsageDescription + An application in Android Studio requests access to files on a network volume. + NSRemovableVolumesUsageDescription + An application in Android Studio requests access to files on a removable volume. + NSSupportsAutomaticGraphicsSwitching + + + +[ +3 ms] executing: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java -version +[ +40 ms] Exit code 0 from: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java -version +[ ] openjdk version "21.0.8" 2025-07-15 + OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + OpenJDK 64-Bit Server VM (build 21.0.8+-14196175-b1038.72, mixed mode) +[ +3 ms] Top-level Gradle build file not found, skipping migration of task "clean". +[ ] executing: /Library/Java/JavaVirtualMachines/openjdk-17.jdk/Contents/Home/bin/java --version +[ +22 ms] Exit code 0 from: /Library/Java/JavaVirtualMachines/openjdk-17.jdk/Contents/Home/bin/java --version +[ ] openjdk 17.0.17 2025-10-21 + OpenJDK Runtime Environment Homebrew (build 17.0.17+0) + OpenJDK 64-Bit Server VM Homebrew (build 17.0.17+0, mixed mode, sharing) +[ +1 ms] CMake project not found, skipping support Android 15 16k page size migration. +[ ] The developer has already configured the Built-In Kotlin flag, skipping migration. +[ ] The developer has already configured the new DSL flag, skipping migration. +[ +1 ms] Using gradle from /Users/anbarasu/Downloads/doormile_flutter/android/gradlew. +[ +6 ms] LocalFile: '/Users/anbarasu/Downloads/doormile_flutter/android/build.gradle.kts' doesn't provide an AGP version. Checking settings. +[ ] LocalFile: '/Users/anbarasu/Downloads/doormile_flutter/android/settings.gradle.kts' provides AGP version: 8.11.1 +[ +6 ms] Running Gradle task 'bundleRelease'... +[ ] executing: [/Users/anbarasu/Downloads/doormile_flutter/android/] /Users/anbarasu/Downloads/doormile_flutter/android/gradlew --full-stacktrace --info -Pverbose=true -Ptarget-platform=android-arm,android-arm64,android-x64 -Ptarget=lib/main.dart -Pbase-application-name=android.app.Application -Pdart-defines=RkxVVFRFUl9WRVJTSU9OPTMuNDQuMA==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NTU5ZmZhM2Y3NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NGM1MjVkYWM1ZQ==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMi4w -Pdart-obfuscation=false -Ptrack-widget-creation=true -Ptree-shake-icons=true bundleRelease +[ +210 ms] Initialized native services in: /Users/anbarasu/.gradle/native +[ ] Initialized jansi services in: /Users/anbarasu/.gradle/native +[ +115 ms] The client will now receive all logging from the daemon (pid: 29613). The daemon log file: /Users/anbarasu/.gradle/daemon/8.14/daemon-29613.out.log +[ ] Starting 5th build in daemon [uptime: 10 mins 51.786 secs, performance: 100%, GC rate: 0.00/s, heap usage: 0% of 6 GiB] +[ ] Using 10 worker leases. +[ ] Now considering [/Users/anbarasu/Downloads/doormile_flutter/android, /Users/anbarasu/flutter/packages/flutter_tools/gradle] as hierarchies to watch +[ ] Watching the file system is configured to be enabled if available +[ ] File system watching is active +[ ] Starting Build +[ ] Now considering [/Users/anbarasu/flutter/packages/flutter_tools/gradle, /Users/anbarasu/Downloads/doormile_flutter/android] as hierarchies to watch +[ +99 ms] > Configure project :gradle +[ ] Evaluating project ':gradle' using build file '/Users/anbarasu/flutter/packages/flutter_tools/gradle/build.gradle.kts'. +[ ] Resolved plugin [id: 'org.gradle.java-gradle-plugin'] +[ ] Resolved plugin [id: 'org.gradle.groovy'] +[ ] Resolved plugin [id: 'org.gradle.kotlin.kotlin-dsl', version: '5.2.0'] +[ ] Resolved plugin [id: 'org.jetbrains.kotlin.jvm', version: '2.2.20'] +[ ] Build b0fbb022-5ca9-44e4-94f6-596ae9452722 is started +[ ] Using Kotlin Gradle Plugin gradle813 variant +[ ] kotlin scripting plugin: created the scripting discovery configuration: kotlinScriptDef +[ ] kotlin scripting plugin: created the scripting discovery configuration: testKotlinScriptDef +[ ] WARNING: Unsupported Kotlin plugin version. +[ ] The `embedded-kotlin` and `kotlin-dsl` plugins rely on features of Kotlin `2.0.21` that might work differently than in the requested version `2.2.20`. +[ ] file or directory '/Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/java', not found +[ ] Resolved plugin [id: 'dev.flutter.flutter-plugin-loader', version: '1.0.0'] +[ ] Resolved plugin [id: 'com.android.application', version: '8.11.1', apply: false] +[ ] Resolved plugin [id: 'org.jetbrains.kotlin.android', version: '2.2.20', apply: false] +[ ] Resolve mutations for :gradle:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker,5,main]) started. +[ ] :gradle:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:checkKotlinGradlePluginConfigurationErrors SKIPPED +[ ] Skipping task ':gradle:checkKotlinGradlePluginConfigurationErrors' as task onlyIf 'errorDiagnostics are present' is false. +[ ] Resolve mutations for :gradle:compileKotlin (Thread[Execution worker,5,main]) started. +[ ] :gradle:compileKotlin (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:compileKotlin UP-TO-DATE +[ ] Caching disabled for task ':gradle:compileKotlin' because: +[ ] Build cache is disabled +[ ] Skipping task ':gradle:compileKotlin' as it is up-to-date. +[ ] Resolve mutations for :gradle:compileJava (Thread[Execution worker,5,main]) started. +[ ] :gradle:compileJava (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:compileJava NO-SOURCE +[ ] Skipping task ':gradle:compileJava' as it has no source files and no previous output files. +[ ] Resolve mutations for :gradle:compileGroovy (Thread[Execution worker,5,main]) started. +[ ] :gradle:compileGroovy (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:compileGroovy NO-SOURCE +[ ] Skipping task ':gradle:compileGroovy' as it has no source files and no previous output files. +[ ] Resolve mutations for :gradle:pluginDescriptors (Thread[Execution worker,5,main]) started. +[ ] :gradle:pluginDescriptors (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:pluginDescriptors UP-TO-DATE +[ ] Caching disabled for task ':gradle:pluginDescriptors' because: +[ ] Build cache is disabled +[ ] Not worth caching +[ ] Skipping task ':gradle:pluginDescriptors' as it is up-to-date. +[ ] Resolve mutations for :gradle:processResources (Thread[Execution worker,5,main]) started. +[ ] :gradle:processResources (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:processResources UP-TO-DATE +[ ] Caching disabled for task ':gradle:processResources' because: +[ ] Build cache is disabled +[ ] Not worth caching +[ ] Skipping task ':gradle:processResources' as it is up-to-date. +[ ] Resolve mutations for :gradle:classes (Thread[Execution worker,5,main]) started. +[ ] :gradle:classes (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:classes UP-TO-DATE +[ ] Skipping task ':gradle:classes' as it has no actions. +[ ] Resolve mutations for :gradle:jar (Thread[Execution worker,5,main]) started. +[ ] :gradle:jar (Thread[Execution worker,5,main]) started. +[ ] > Task :gradle:jar UP-TO-DATE +[ ] Caching disabled for task ':gradle:jar' because: +[ ] Build cache is disabled +[ ] Not worth caching +[ ] Skipping task ':gradle:jar' as it is up-to-date. +[ +95 ms] Settings evaluated using settings file '/Users/anbarasu/Downloads/doormile_flutter/android/settings.gradle.kts'. +[ ] Projects loaded. Root project using build file '/Users/anbarasu/Downloads/doormile_flutter/android/build.gradle.kts'. +[ ] Included projects: [root project 'android', project ':app', project ':geolocator_android', project ':jni', project ':jni_flutter'] +[ ] > Configure project :app +[ ] Evaluating project ':app' using build file '/Users/anbarasu/Downloads/doormile_flutter/android/app/build.gradle.kts'. +[ ] Resolved plugin [id: 'com.android.application'] +[ ] Resolved plugin [id: 'kotlin-android'] +[ ] Resolved plugin [id: 'dev.flutter.flutter-gradle-plugin'] +[ ] Using default execution profile +[ ] Build 982634eb-17c1-4374-b8bf-069dc6d4837f is started +[ ] Using Kotlin Gradle Plugin gradle813 variant +[ ] Using Kotlin Gradle Plugin gradle813 variant +[ ] Using Kotlin Gradle Plugin gradle813 variant +[ ] Using Kotlin Gradle Plugin gradle813 variant +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ +1 ms] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] > Configure project : +[ ] Evaluating root project 'android' using build file '/Users/anbarasu/Downloads/doormile_flutter/android/build.gradle.kts'. +[ +95 ms] > Configure project :geolocator_android +[ ] Evaluating project ':geolocator_android' using build file '/Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/android/build.gradle'. +[ ] Using default execution profile +[ ] > Configure project :jni +[ ] Evaluating project ':jni' using build file '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/build.gradle'. +[ ] Using default execution profile +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] C/C++: android.ndkVersion from module build.gradle is [28.2.13676358] +[ ] C/C++: android.ndkPath from module build.gradle is not set +[ ] C/C++: ndk.dir in local.properties is not set +[ ] C/C++: Not considering ANDROID_NDK_HOME because support was removed after deprecation period. +[ ] C/C++: sdkFolder is /Users/anbarasu/Library/Android/sdk +[ ] > Configure project :jni_flutter +[ ] Evaluating project ':jni_flutter' using build file '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/android/build.gradle'. +[ ] Using default execution profile +[ ] All projects evaluated. +[ ] Task name matched 'bundleRelease' +[ ] Selected primary task 'bundleRelease' from project : +[ ] WARNING: Your Android app project: app located at: /Users/anbarasu/Downloads/doormile_flutter/android/app/build.gradle.kts +[ ] applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter. +[ ] Please migrate your app to Built-in Kotlin using this guide: https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers +[ +97 ms] Tasks to be executed: [task ':app:preBuild', task ':app:preReleaseBuild', task ':app:generateReleaseResValues', task ':geolocator_android:preBuild', task ':geolocator_android:preReleaseBuild', task ':geolocator_android:generateReleaseResValues', task ':geolocator_android:generateReleaseResources', task ':geolocator_android:packageReleaseResources', task ':jni:preBuild', task ':jni:preReleaseBuild', task ':jni:generateReleaseResValues', task ':jni:generateReleaseResources', task ':jni:packageReleaseResources', task ':jni_flutter:preBuild', task ':jni_flutter:preReleaseBuild', task ':jni_flutter:generateReleaseResValues', task ':jni_flutter:generateReleaseResources', task ':jni_flutter:packageReleaseResources', task ':app:mapReleaseSourceSetPaths', task ':app:generateReleaseResources', task ':app:mergeReleaseResources', task ':app:createReleaseCompatibleScreenManifests', task ':app:extractDeepLinksRelease', task ':geolocator_android:extractDeepLinksRelease', task ':geolocator_android:processReleaseManifest', task ':jni:extractDeepLinksRelease', task ':jni:processReleaseManifest', task ':jni_flutter:extractDeepLinksRelease', task ':jni_flutter:processReleaseManifest', task ':app:processReleaseMainManifest', task ':app:outputReleaseAppLinkSettings', task ':app:processReleaseManifest', task ':app:processApplicationManifestReleaseForBundle', task ':geolocator_android:writeReleaseAarMetadata', task ':jni:writeReleaseAarMetadata', task ':jni_flutter:writeReleaseAarMetadata', task ':app:checkReleaseAarMetadata', task ':app:cleanMergeReleaseAssets', task ':app:compileFlutterBuildRelease', task ':app:mergeReleaseShaders', task ':app:compileReleaseShaders', task ':app:generateReleaseAssets', task ':geolocator_android:mergeReleaseShaders', task ':geolocator_android:compileReleaseShaders', task ':geolocator_android:generateReleaseAssets', task ':geolocator_android:mergeReleaseAssets', task ':jni:mergeReleaseShaders', task ':jni:compileReleaseShaders', task ':jni:generateReleaseAssets', task ':jni:mergeReleaseAssets', task ':jni_flutter:mergeReleaseShaders', task ':jni_flutter:compileReleaseShaders', task ':jni_flutter:generateReleaseAssets', task ':jni_flutter:mergeReleaseAssets', task ':app:mergeReleaseAssets', task ':app:copyFlutterAssetsRelease', task ':app:packageReleaseResources', task ':app:parseReleaseLocalResources', task ':app:processReleaseManifestForPackage', task ':geolocator_android:compileReleaseLibraryResources', task ':geolocator_android:parseReleaseLocalResources', task ':geolocator_android:generateReleaseRFile', task ':jni:compileReleaseLibraryResources', task ':jni:parseReleaseLocalResources', task ':jni:generateReleaseRFile', task ':jni_flutter:compileReleaseLibraryResources', task ':jni_flutter:parseReleaseLocalResources', task ':jni_flutter:generateReleaseRFile', task ':app:processReleaseResources', task ':app:bundleReleaseResources', task ':app:extractReleaseVersionControlInfo', task ':geolocator_android:checkKotlinGradlePluginConfigurationErrors', task ':geolocator_android:compileReleaseKotlin', task ':geolocator_android:javaPreCompileRelease', task ':geolocator_android:compileReleaseJavaWithJavac', task ':geolocator_android:bundleLibRuntimeToDirRelease', task ':jni:checkKotlinGradlePluginConfigurationErrors', task ':jni:compileReleaseKotlin', task ':jni:javaPreCompileRelease', task ':jni:compileReleaseJavaWithJavac', task ':jni:bundleLibCompileToJarRelease', task ':jni_flutter:checkKotlinGradlePluginConfigurationErrors', task ':jni_flutter:compileReleaseKotlin', task ':jni_flutter:javaPreCompileRelease', task ':jni_flutter:compileReleaseJavaWithJavac', task ':jni_flutter:bundleLibRuntimeToDirRelease', task ':jni:bundleLibRuntimeToDirRelease', task ':app:checkReleaseDuplicateClasses', task ':app:buildKotlinToolingMetadata', task ':app:checkKotlinGradlePluginConfigurationErrors', task ':geolocator_android:bundleLibCompileToJarRelease', task ':jni_flutter:bundleLibCompileToJarRelease', task ':app:compileReleaseKotlin', task ':app:javaPreCompileRelease', task ':app:compileReleaseJavaWithJavac', task ':app:dexBuilderRelease', task ':app:desugarReleaseFileDependencies', task ':app:mergeReleaseStartupProfile', task ':app:mergeExtDexRelease', task ':app:mergeDexRelease', task ':geolocator_android:bundleLibRuntimeToJarRelease', task ':jni_flutter:bundleLibRuntimeToJarRelease', task ':jni:bundleLibRuntimeToJarRelease', task ':app:mergeReleaseGlobalSynthetics', task ':app:processReleaseJavaRes', task ':geolocator_android:processReleaseJavaRes', task ':jni:processReleaseJavaRes', task ':jni_flutter:processReleaseJavaRes', task ':app:mergeReleaseJavaResource', task ':app:configureCMakeRelease[arm64-v8a]', task ':app:buildCMakeRelease[arm64-v8a]', task ':app:configureCMakeRelease[armeabi-v7a]', task ':app:buildCMakeRelease[armeabi-v7a]', task ':app:configureCMakeRelease[x86_64]', task ':app:buildCMakeRelease[x86_64]', task ':app:copyJniLibsflutterBuildRelease', task ':app:mergeReleaseJniLibFolders', task ':geolocator_android:mergeReleaseJniLibFolders', task ':geolocator_android:mergeReleaseNativeLibs', task ':geolocator_android:copyReleaseJniLibsProjectOnly', task ':jni:configureCMakeRelWithDebInfo[arm64-v8a]', task ':jni:buildCMakeRelWithDebInfo[arm64-v8a]', task ':jni:configureCMakeRelWithDebInfo[armeabi-v7a]', task ':jni:buildCMakeRelWithDebInfo[armeabi-v7a]', task ':jni:configureCMakeRelWithDebInfo[x86]', task ':jni:buildCMakeRelWithDebInfo[x86]', task ':jni:configureCMakeRelWithDebInfo[x86_64]', task ':jni:buildCMakeRelWithDebInfo[x86_64]', task ':jni:mergeReleaseJniLibFolders', task ':jni:mergeReleaseNativeLibs', task ':jni:copyReleaseJniLibsProjectOnly', task ':jni_flutter:mergeReleaseJniLibFolders', task ':jni_flutter:mergeReleaseNativeLibs', task ':jni_flutter:copyReleaseJniLibsProjectOnly', task ':app:mergeReleaseNativeLibs', task ':app:stripReleaseDebugSymbols', task ':app:writeReleaseAppMetadata', task ':app:buildReleasePreBundle', task ':geolocator_android:prepareReleaseArtProfile', task ':jni:prepareReleaseArtProfile', task ':jni_flutter:prepareReleaseArtProfile', task ':app:mergeReleaseArtProfile', task ':app:compileReleaseArtProfile', task ':app:collectReleaseDependencies', task ':app:configureReleaseDependencies', task ':app:extractReleaseNativeSymbolTables', task ':app:extractProguardFiles', task ':geolocator_android:createFullJarRelease', task ':geolocator_android:extractProguardFiles', task ':geolocator_android:generateReleaseLintModel', task ':geolocator_android:prepareLintJarForPublish', task ':jni:createFullJarRelease', task ':jni:extractProguardFiles', task ':jni:generateReleaseLintModel', task ':jni:prepareLintJarForPublish', task ':jni_flutter:createFullJarRelease', task ':jni_flutter:extractProguardFiles', task ':jni_flutter:generateReleaseLintModel', task ':jni_flutter:prepareLintJarForPublish', task ':app:generateReleaseLintVitalReportModel', task ':geolocator_android:stripReleaseDebugSymbols', task ':geolocator_android:copyReleaseJniLibsProjectAndLocalJars', task ':geolocator_android:extractDeepLinksForAarRelease', task ':geolocator_android:extractReleaseAnnotations', task ':geolocator_android:mergeReleaseGeneratedProguardFiles', task ':geolocator_android:mergeReleaseConsumerProguardFiles', task ':geolocator_android:mergeReleaseJavaResource', task ':geolocator_android:syncReleaseLibJars', task ':geolocator_android:bundleReleaseLocalLintAar', task ':jni_flutter:stripReleaseDebugSymbols', task ':jni_flutter:copyReleaseJniLibsProjectAndLocalJars', task ':jni_flutter:extractDeepLinksForAarRelease', task ':jni_flutter:extractReleaseAnnotations', task ':jni_flutter:mergeReleaseGeneratedProguardFiles', task ':jni_flutter:mergeReleaseConsumerProguardFiles', task ':jni_flutter:mergeReleaseJavaResource', task ':jni_flutter:syncReleaseLibJars', task ':jni_flutter:bundleReleaseLocalLintAar', task ':jni:stripReleaseDebugSymbols', task ':jni:copyReleaseJniLibsProjectAndLocalJars', task ':jni:extractDeepLinksForAarRelease', task ':jni:extractReleaseAnnotations', task ':jni:mergeReleaseGeneratedProguardFiles', task ':jni:mergeReleaseConsumerProguardFiles', task ':jni:mergeReleaseJavaResource', task ':jni:syncReleaseLibJars', task ':jni:bundleReleaseLocalLintAar', task ':geolocator_android:lintVitalAnalyzeRelease', task ':geolocator_android:writeReleaseLintModelMetadata', task ':jni:lintVitalAnalyzeRelease', task ':jni:writeReleaseLintModelMetadata', task ':jni_flutter:lintVitalAnalyzeRelease', task ':jni_flutter:writeReleaseLintModelMetadata', task ':app:lintVitalAnalyzeRelease', task ':geolocator_android:generateReleaseLintVitalModel', task ':jni:generateReleaseLintVitalModel', task ':jni_flutter:generateReleaseLintVitalModel', task ':app:lintVitalReportRelease', task ':app:lintVitalRelease', task ':app:parseReleaseIntegrityConfig', task ':app:packageReleaseBundle', task ':app:validateSigningRelease', task ':app:signReleaseBundle', task ':app:produceReleaseBundleIdeListingFile', task ':app:createReleaseBundleListingFileRedirect', task ':app:bundleRelease'] +[ ] Tasks that were excluded: [] +[ ] work action Parameters of ClassesDirToClassesTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action Parameters of DexingNoClasspathTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action Parameters of DexingOutputSplitTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action Parameters of IdentityTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action Parameters of DexingOutputSplitTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action Parameters of ExtractAarTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action Parameters of ExtractAarTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :app:preBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:preBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:preBuild UP-TO-DATE +[ ] Skipping task ':app:preBuild' as it has no actions. +[ ] Resolve mutations for :app:preReleaseBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:preReleaseBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:preReleaseBuild UP-TO-DATE +[ ] Skipping task ':app:preReleaseBuild' as it has no actions. +[ ] Resolve mutations for :app:generateReleaseResValues (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:generateReleaseResValues (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:generateReleaseResValues UP-TO-DATE +[ ] Caching disabled for task ':app:generateReleaseResValues' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:generateReleaseResValues' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:preBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:preBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :geolocator_android:preBuild UP-TO-DATE +[ ] Skipping task ':geolocator_android:preBuild' as it has no actions. +[ ] Resolve mutations for :geolocator_android:preReleaseBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:preReleaseBuild (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :geolocator_android:preReleaseBuild UP-TO-DATE +[ ] Skipping task ':geolocator_android:preReleaseBuild' as it has no actions. +[ ] Resolve mutations for :geolocator_android:generateReleaseResValues (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:generateReleaseResValues (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :geolocator_android:generateReleaseResValues UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:generateReleaseResValues' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:generateReleaseResValues' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:generateReleaseResources (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:generateReleaseResources (Thread[included builds,5,main]) started. +[ ] > Task :geolocator_android:generateReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:generateReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:generateReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:packageReleaseResources (Thread[included builds,5,main]) started. +[ ] :geolocator_android:packageReleaseResources (Thread[included builds,5,main]) started. +[ ] > Task :geolocator_android:packageReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:packageReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:packageReleaseResources' as it is up-to-date. +[ ] work action resolve packageReleaseResources (project :geolocator_android) (Thread[included builds,5,main]) started. +[ ] Resolve mutations for :jni:preBuild (Thread[included builds,5,main]) started. +[ ] :jni:preBuild (Thread[included builds,5,main]) started. +[ ] > Task :jni:preBuild UP-TO-DATE +[ ] Skipping task ':jni:preBuild' as it has no actions. +[ ] Resolve mutations for :jni:preReleaseBuild (Thread[included builds,5,main]) started. +[ ] :jni:preReleaseBuild (Thread[included builds,5,main]) started. +[ ] > Task :jni:preReleaseBuild UP-TO-DATE +[ ] Skipping task ':jni:preReleaseBuild' as it has no actions. +[ ] Resolve mutations for :jni:generateReleaseResValues (Thread[included builds,5,main]) started. +[ ] :jni:generateReleaseResValues (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:generateReleaseResValues UP-TO-DATE +[ ] Caching disabled for task ':jni:generateReleaseResValues' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:generateReleaseResValues' as it is up-to-date. +[ ] Resolve mutations for :jni:generateReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:generateReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:generateReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':jni:generateReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:generateReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :jni:packageReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:packageReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:packageReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':jni:packageReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:packageReleaseResources' as it is up-to-date. +[ ] work action resolve packageReleaseResources (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:preBuild (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:preBuild (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:preBuild UP-TO-DATE +[ ] Skipping task ':jni_flutter:preBuild' as it has no actions. +[ ] Resolve mutations for :jni_flutter:preReleaseBuild (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:preReleaseBuild (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:preReleaseBuild UP-TO-DATE +[ ] Skipping task ':jni_flutter:preReleaseBuild' as it has no actions. +[ ] Resolve mutations for :jni_flutter:generateReleaseResValues (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:generateReleaseResValues (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:generateReleaseResValues UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:generateReleaseResValues' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:generateReleaseResValues' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:generateReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:generateReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:generateReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:generateReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:generateReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:packageReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:packageReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:packageReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:packageReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:packageReleaseResources' as it is up-to-date. +[ ] work action resolve packageReleaseResources (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :app:mapReleaseSourceSetPaths (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:mapReleaseSourceSetPaths (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:mapReleaseSourceSetPaths UP-TO-DATE +[ ] Caching disabled for task ':app:mapReleaseSourceSetPaths' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:mapReleaseSourceSetPaths' as it is up-to-date. +[ ] Resolve mutations for :app:generateReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:generateReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:generateReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':app:generateReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:generateReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :app:mergeReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:mergeReleaseResources (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:mergeReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:mergeReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :app:createReleaseCompatibleScreenManifests (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:createReleaseCompatibleScreenManifests (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:createReleaseCompatibleScreenManifests UP-TO-DATE +[ ] Caching disabled for task ':app:createReleaseCompatibleScreenManifests' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:createReleaseCompatibleScreenManifests' as it is up-to-date. +[ ] Resolve mutations for :app:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:extractDeepLinksRelease UP-TO-DATE +[ ] Caching disabled for task ':app:extractDeepLinksRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:extractDeepLinksRelease' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:extractDeepLinksRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:extractDeepLinksRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:extractDeepLinksRelease' as it is up-to-date. +[ ] work action resolve navigation.json (project :geolocator_android) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :geolocator_android:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:processReleaseManifest UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:processReleaseManifest' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:processReleaseManifest' as it is up-to-date. +[ ] work action resolve AndroidManifest.xml (project :geolocator_android) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:extractDeepLinksRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:extractDeepLinksRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:extractDeepLinksRelease' as it is up-to-date. +[ ] work action resolve navigation.json (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:processReleaseManifest UP-TO-DATE +[ ] Caching disabled for task ':jni:processReleaseManifest' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:processReleaseManifest' as it is up-to-date. +[ ] work action resolve AndroidManifest.xml (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:extractDeepLinksRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:extractDeepLinksRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:extractDeepLinksRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:extractDeepLinksRelease' as it is up-to-date. +[ ] work action resolve navigation.json (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:processReleaseManifest UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:processReleaseManifest' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:processReleaseManifest' as it is up-to-date. +[ ] work action resolve AndroidManifest.xml (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :app:processReleaseMainManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:processReleaseMainManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:processReleaseMainManifest UP-TO-DATE +[ ] Caching disabled for task ':app:processReleaseMainManifest' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:processReleaseMainManifest' as it is up-to-date. +[ ] Resolve mutations for :app:outputReleaseAppLinkSettings (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:outputReleaseAppLinkSettings (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:outputReleaseAppLinkSettings UP-TO-DATE +[ ] Caching disabled for task ':app:outputReleaseAppLinkSettings' because: +[ ] Build cache is disabled +[ ] Caching has not been enabled for the task +[ ] Skipping task ':app:outputReleaseAppLinkSettings' as it is up-to-date. +[ ] Resolve mutations for :app:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:processReleaseManifest (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:processReleaseManifest UP-TO-DATE +[ ] Caching disabled for task ':app:processReleaseManifest' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:processReleaseManifest' as it is up-to-date. +[ ] Resolve mutations for :app:processApplicationManifestReleaseForBundle (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:processApplicationManifestReleaseForBundle (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:processApplicationManifestReleaseForBundle UP-TO-DATE +[ ] Caching disabled for task ':app:processApplicationManifestReleaseForBundle' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:processApplicationManifestReleaseForBundle' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:writeReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:writeReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:writeReleaseAarMetadata UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:writeReleaseAarMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:writeReleaseAarMetadata' as it is up-to-date. +[ ] work action resolve aar-metadata.properties (project :geolocator_android) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni:writeReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:writeReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:writeReleaseAarMetadata UP-TO-DATE +[ ] Caching disabled for task ':jni:writeReleaseAarMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:writeReleaseAarMetadata' as it is up-to-date. +[ ] work action resolve aar-metadata.properties (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:writeReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:writeReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:writeReleaseAarMetadata UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:writeReleaseAarMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:writeReleaseAarMetadata' as it is up-to-date. +[ ] work action resolve aar-metadata.properties (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :app:checkReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:checkReleaseAarMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:checkReleaseAarMetadata UP-TO-DATE +[ ] Caching disabled for task ':app:checkReleaseAarMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:checkReleaseAarMetadata' as it is up-to-date. +[ ] Resolve mutations for :app:cleanMergeReleaseAssets (Thread[Execution worker Thread 9,5,main]) started. +[ +90 ms] :app:cleanMergeReleaseAssets (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:cleanMergeReleaseAssets +[ ] Caching disabled for task ':app:cleanMergeReleaseAssets' because: +[ ] Build cache is disabled +[ ] Task ':app:cleanMergeReleaseAssets' is not up-to-date because: +[ ] Task has not declared any outputs despite executing actions. +[ ] Resolve mutations for :app:compileFlutterBuildRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:compileFlutterBuildRelease (Thread[Execution worker Thread 9,5,main]) started. +[+1199 ms] > Task :app:compileFlutterBuildRelease +[ ] Caching disabled for task ':app:compileFlutterBuildRelease' because: +[ ] Build cache is disabled +[ ] Caching has not been enabled for the task +[ ] Task ':app:compileFlutterBuildRelease' is not up-to-date because: +[ ] Value of input property 'verbose' has changed for task ':app:compileFlutterBuildRelease' +[ ] Starting process 'command '/Users/anbarasu/flutter/bin/flutter''. Working directory: /Users/anbarasu/Downloads/doormile_flutter Command: /Users/anbarasu/flutter/bin/flutter --verbose assemble --no-version-check --depfile /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/flutter/release/flutter_build.d --output /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/flutter/release -dTargetFile=lib/main.dart -dTargetPlatform=android -dBuildMode=release -dTrackWidgetCreation=true -dTreeShakeIcons=true --DartDefines=RkxVVFRFUl9WRVJTSU9OPTMuNDQuMA==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NTU5ZmZhM2Y3NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NGM1MjVkYWM1ZQ==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMi4w -dFlavor= -dAndroidArchs=android-arm android-arm64 android-x64 -dMinSdkVersion=24 android_aot_bundle_release_android-arm android_aot_bundle_release_android-arm64 android_aot_bundle_release_android-x64 +[ ] Successfully started process 'command '/Users/anbarasu/flutter/bin/flutter'' +[ ] [ +12 ms] executing: sysctl hw.optional.arm64 +[ ] [ +4 ms] Exit code 0 from: sysctl hw.optional.arm64 +[ ] [ ] hw.optional.arm64: 1 +[ ] [ +3 ms] Artifact Instance of 'MaterialFonts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'GradleWrapper' is not required, skipping update. +[ ] [ ] Artifact Instance of 'AndroidGenSnapshotArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'AndroidInternalBuildArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IOSEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterWebSdk' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LegacyCanvasKitRemover' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterSdk' is not required, skipping update. +[ ] [ ] Artifact Instance of 'WindowsEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'MacOSEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LinuxEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LinuxFuchsiaSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'MacOSFuchsiaSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterRunnerSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterRunnerDebugSymbols' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FontSubsetArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'PubDependencies' is not required, skipping update. +[ ] [ +3 ms] Artifact Instance of 'AndroidGenSnapshotArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'AndroidInternalBuildArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IOSEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterWebSdk' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterEngineStamp' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LegacyCanvasKitRemover' is not required, skipping update. +[ ] [ ] Artifact Instance of 'WindowsEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'MacOSEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LinuxEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LinuxFuchsiaSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'MacOSFuchsiaSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterRunnerSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterRunnerDebugSymbols' is not required, skipping update. +[ ] [ +17 ms] Artifact Instance of 'MaterialFonts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'GradleWrapper' is not required, skipping update. +[ ] [ ] Artifact Instance of 'AndroidInternalBuildArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IOSEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterWebSdk' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterEngineStamp' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LegacyCanvasKitRemover' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterSdk' is not required, skipping update. +[ ] [ ] Artifact Instance of 'WindowsEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'MacOSEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LinuxEngineArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'LinuxFuchsiaSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'MacOSFuchsiaSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterRunnerSDKArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FlutterRunnerDebugSymbols' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'IosUsbArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'FontSubsetArtifacts' is not required, skipping update. +[ ] [ ] Artifact Instance of 'PubDependencies' is not required, skipping update. +[ ] [ +16 ms] Initializing file store +[ ] [ +4 ms] Done initializing file store +[ ] [ +55 ms] Skipping target: gen_dart_plugin_registrant +[ ] [ +1 ms] Skipping target: dart_build +[ ] [ ] Skipping target: gen_localizations +[ ] [ ] Skipping target: install_code_assets +[ ] [ +449 ms] Skipping target: kernel_snapshot_program +[ ] [ +34 ms] Skipping target: android_aot_release_android-arm +[ ] [ +30 ms] Skipping target: aot_android_asset_bundle +[ ] [ +31 ms] Skipping target: android_aot_release_android-arm64 +[ ] [ +31 ms] Skipping target: android_aot_release_android-x64 +[ ] [ +33 ms] Skipping target: android_aot_bundle_release_android-arm +[ ] [ +32 ms] Skipping target: android_aot_bundle_release_android-arm64 +[ ] [ +33 ms] Skipping target: android_aot_bundle_release_android-x64 +[ ] [ ] Skipping target: _composite +[ ] [ ] Persisting file store +[ ] [ +3 ms] Done persisting file store +[ ] [ +5 ms] build succeeded. +[ ] [ +2 ms] "flutter assemble" took 789ms. +[ ] [ +2 ms] executing: sw_vers -productName +[ ] [ +10 ms] Exit code 0 from: sw_vers -productName +[ ] [ ] macOS +[ ] [ ] executing: sw_vers -productVersion +[ ] [ +7 ms] Exit code 0 from: sw_vers -productVersion +[ ] [ ] 26.4.1 +[ ] [ ] executing: sw_vers -buildVersion +[ ] [ +11 ms] Exit code 0 from: sw_vers -buildVersion +[ ] [ ] 25E253 +[ ] [ ] executing: uname -m +[ ] [ +6 ms] Exit code 0 from: uname -m +[ ] [ ] arm64 +[ ] [ +21 ms] Running 1 shutdown hook +[ ] [ ] Shutdown hooks complete +[ ] [ +257 ms] exiting with code 0 +[ ] Resolve mutations for :app:mergeReleaseShaders (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:mergeReleaseShaders (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:mergeReleaseShaders UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseShaders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':app:mergeReleaseShaders' as it is up-to-date. +[ ] Resolve mutations for :app:compileReleaseShaders (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:compileReleaseShaders (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:compileReleaseShaders NO-SOURCE +[ ] Skipping task ':app:compileReleaseShaders' as it has no source files and no previous output files. +[ ] Resolve mutations for :app:generateReleaseAssets (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:generateReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:generateReleaseAssets UP-TO-DATE +[ ] Skipping task ':app:generateReleaseAssets' as it has no actions. +[ ] Resolve mutations for :geolocator_android:mergeReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:mergeReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseShaders UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:mergeReleaseShaders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:mergeReleaseShaders' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:compileReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:compileReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:compileReleaseShaders NO-SOURCE +[ ] Skipping task ':geolocator_android:compileReleaseShaders' as it has no source files and no previous output files. +[ ] Resolve mutations for :geolocator_android:generateReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:generateReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:generateReleaseAssets UP-TO-DATE +[ ] Skipping task ':geolocator_android:generateReleaseAssets' as it has no actions. +[ ] Resolve mutations for :geolocator_android:mergeReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:mergeReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseAssets UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:mergeReleaseAssets' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:mergeReleaseAssets' as it is up-to-date. +[ ] work action resolve mergeReleaseAssets (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:mergeReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:mergeReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:mergeReleaseShaders UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseShaders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:mergeReleaseShaders' as it is up-to-date. +[ ] Resolve mutations for :jni:compileReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:compileReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:compileReleaseShaders NO-SOURCE +[ ] Skipping task ':jni:compileReleaseShaders' as it has no source files and no previous output files. +[ ] Resolve mutations for :jni:generateReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:generateReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:generateReleaseAssets UP-TO-DATE +[ ] Skipping task ':jni:generateReleaseAssets' as it has no actions. +[ ] Resolve mutations for :jni:mergeReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:mergeReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:mergeReleaseAssets UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseAssets' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:mergeReleaseAssets' as it is up-to-date. +[ ] work action resolve mergeReleaseAssets (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni_flutter:mergeReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:mergeReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseShaders UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:mergeReleaseShaders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:mergeReleaseShaders' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:compileReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:compileReleaseShaders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:compileReleaseShaders NO-SOURCE +[ ] Skipping task ':jni_flutter:compileReleaseShaders' as it has no source files and no previous output files. +[ ] Resolve mutations for :jni_flutter:generateReleaseAssets (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:generateReleaseAssets (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni_flutter:generateReleaseAssets UP-TO-DATE +[ ] Skipping task ':jni_flutter:generateReleaseAssets' as it has no actions. +[ ] Resolve mutations for :jni_flutter:mergeReleaseAssets (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni_flutter:mergeReleaseAssets (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseAssets UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:mergeReleaseAssets' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:mergeReleaseAssets' as it is up-to-date. +[ ] work action resolve mergeReleaseAssets (project :jni_flutter) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :app:mergeReleaseAssets (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:mergeReleaseAssets (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:mergeReleaseAssets +[ ] Caching disabled for task ':app:mergeReleaseAssets' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Task ':app:mergeReleaseAssets' is not up-to-date because: +[ ] Output property 'outputDir' file /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/assets/release/mergeReleaseAssets has been removed. +[ ] The input changes require a full rebuild for incremental task ':app:mergeReleaseAssets'. +[ ] Resolve mutations for :app:copyFlutterAssetsRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:copyFlutterAssetsRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:copyFlutterAssetsRelease +[ ] Caching disabled for task ':app:copyFlutterAssetsRelease' because: +[ ] Build cache is disabled +[ ] Not worth caching +[ ] Task ':app:copyFlutterAssetsRelease' is not up-to-date because: +[ ] Output property 'destinationDir' file /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/assets/release/mergeReleaseAssets/flutter_assets has been removed. +[ ] Output property 'destinationDir' file /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/assets/release/mergeReleaseAssets/flutter_assets/AssetManifest.bin has been removed. +[ ] Output property 'destinationDir' file /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/assets/release/mergeReleaseAssets/flutter_assets/FontManifest.json has been removed. +[ ] and more... +[ ] Resolve mutations for :app:packageReleaseResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:packageReleaseResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:packageReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':app:packageReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:packageReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :app:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:parseReleaseLocalResources UP-TO-DATE +[ ] Caching disabled for task ':app:parseReleaseLocalResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:parseReleaseLocalResources' as it is up-to-date. +[ ] Resolve mutations for :app:processReleaseManifestForPackage (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:processReleaseManifestForPackage (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:processReleaseManifestForPackage UP-TO-DATE +[ ] Custom actions are attached to task ':app:processReleaseManifestForPackage'. +[ ] Caching disabled for task ':app:processReleaseManifestForPackage' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:processReleaseManifestForPackage' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:compileReleaseLibraryResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :geolocator_android:compileReleaseLibraryResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :geolocator_android:compileReleaseLibraryResources UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:compileReleaseLibraryResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:compileReleaseLibraryResources' as it is up-to-date. +[ ] work action resolve out (project :geolocator_android) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :geolocator_android:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :geolocator_android:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :geolocator_android:parseReleaseLocalResources UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:parseReleaseLocalResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:parseReleaseLocalResources' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:generateReleaseRFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] :geolocator_android:generateReleaseRFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :geolocator_android:generateReleaseRFile UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:generateReleaseRFile' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:generateReleaseRFile' as it is up-to-date. +[ ] work action resolve package-aware-r.txt (project :geolocator_android) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :jni:compileReleaseLibraryResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni:compileReleaseLibraryResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni:compileReleaseLibraryResources UP-TO-DATE +[ ] Caching disabled for task ':jni:compileReleaseLibraryResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:compileReleaseLibraryResources' as it is up-to-date. +[ ] work action resolve out (project :jni) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :jni:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni:parseReleaseLocalResources UP-TO-DATE +[ ] Caching disabled for task ':jni:parseReleaseLocalResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:parseReleaseLocalResources' as it is up-to-date. +[ ] Resolve mutations for :jni:generateReleaseRFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni:generateReleaseRFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni:generateReleaseRFile UP-TO-DATE +[ ] Caching disabled for task ':jni:generateReleaseRFile' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:generateReleaseRFile' as it is up-to-date. +[ ] work action resolve package-aware-r.txt (project :jni) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :jni_flutter:compileReleaseLibraryResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni_flutter:compileReleaseLibraryResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni_flutter:compileReleaseLibraryResources UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:compileReleaseLibraryResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:compileReleaseLibraryResources' as it is up-to-date. +[ ] work action resolve out (project :jni_flutter) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :jni_flutter:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni_flutter:parseReleaseLocalResources (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni_flutter:parseReleaseLocalResources UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:parseReleaseLocalResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:parseReleaseLocalResources' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:generateReleaseRFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] :jni_flutter:generateReleaseRFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :jni_flutter:generateReleaseRFile UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:generateReleaseRFile' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:generateReleaseRFile' as it is up-to-date. +[ ] work action resolve package-aware-r.txt (project :jni_flutter) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :app:processReleaseResources (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:processReleaseResources (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:processReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':app:processReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:processReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :app:bundleReleaseResources (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:bundleReleaseResources (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:bundleReleaseResources UP-TO-DATE +[ ] Caching disabled for task ':app:bundleReleaseResources' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:bundleReleaseResources' as it is up-to-date. +[ ] Resolve mutations for :app:extractReleaseVersionControlInfo (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:extractReleaseVersionControlInfo (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:extractReleaseVersionControlInfo UP-TO-DATE +[ ] Caching disabled for task ':app:extractReleaseVersionControlInfo' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:extractReleaseVersionControlInfo' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:checkKotlinGradlePluginConfigurationErrors SKIPPED +[ ] Skipping task ':geolocator_android:checkKotlinGradlePluginConfigurationErrors' as task onlyIf 'errorDiagnostics are present' is false. +[ ] Resolve mutations for :geolocator_android:compileReleaseKotlin (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:compileReleaseKotlin (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:compileReleaseKotlin NO-SOURCE +[ ] Skipping task ':geolocator_android:compileReleaseKotlin' as it has no source files and no previous output files. +[ ] Resolve mutations for :geolocator_android:javaPreCompileRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:javaPreCompileRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:javaPreCompileRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:javaPreCompileRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:javaPreCompileRelease' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:compileReleaseJavaWithJavac (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:compileReleaseJavaWithJavac (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:compileReleaseJavaWithJavac UP-TO-DATE +[ ] Custom actions are attached to task ':geolocator_android:compileReleaseJavaWithJavac'. +[ ] Caching disabled for task ':geolocator_android:compileReleaseJavaWithJavac' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:compileReleaseJavaWithJavac' as it is up-to-date. +[ ] No compile result for :geolocator_android:compileReleaseJavaWithJavac +[ ] No compile result for :geolocator_android:compileReleaseJavaWithJavac +[ ] No compile result for :geolocator_android:compileReleaseJavaWithJavac +[ ] Resolve mutations for :geolocator_android:bundleLibRuntimeToDirRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:bundleLibRuntimeToDirRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:bundleLibRuntimeToDirRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:bundleLibRuntimeToDirRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:bundleLibRuntimeToDirRelease' as it is up-to-date. +[ ] work action resolve bundleLibRuntimeToDirRelease (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] ClassesDirToClassesTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni:checkKotlinGradlePluginConfigurationErrors SKIPPED +[ ] Skipping task ':jni:checkKotlinGradlePluginConfigurationErrors' as task onlyIf 'errorDiagnostics are present' is false. +[ ] DexingNoClasspathTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:compileReleaseKotlin (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni:compileReleaseKotlin (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Transform bundleLibRuntimeToDirRelease (project :geolocator_android) with DexingNoClasspathTransform +[ ] Caching disabled for DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/geolocator_android/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease because: +[ ] Build cache is disabled +[ ] Skipping DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/geolocator_android/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease as it is up-to-date. +[ ] DexingOutputSplitTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:compileReleaseKotlin NO-SOURCE +[ ] Skipping task ':jni:compileReleaseKotlin' as it has no source files and no previous output files. +[ ] Resolve mutations for :jni:javaPreCompileRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni:javaPreCompileRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni:javaPreCompileRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:javaPreCompileRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:javaPreCompileRelease' as it is up-to-date. +[ ] Resolve mutations for :jni:compileReleaseJavaWithJavac (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni:compileReleaseJavaWithJavac (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni:compileReleaseJavaWithJavac UP-TO-DATE +[ ] Custom actions are attached to task ':jni:compileReleaseJavaWithJavac'. +[ ] Caching disabled for task ':jni:compileReleaseJavaWithJavac' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:compileReleaseJavaWithJavac' as it is up-to-date. +[ ] No compile result for :jni:compileReleaseJavaWithJavac +[ ] No compile result for :jni:compileReleaseJavaWithJavac +[ ] No compile result for :jni:compileReleaseJavaWithJavac +[ ] Resolve mutations for :jni:bundleLibCompileToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni:bundleLibCompileToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni:bundleLibCompileToJarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:bundleLibCompileToJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:bundleLibCompileToJarRelease' as it is up-to-date. +[ ] work action resolve classes.jar (project :jni) (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :jni_flutter:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni_flutter:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni_flutter:checkKotlinGradlePluginConfigurationErrors SKIPPED +[ ] Skipping task ':jni_flutter:checkKotlinGradlePluginConfigurationErrors' as task onlyIf 'errorDiagnostics are present' is false. +[ ] Resolve mutations for :jni_flutter:compileReleaseKotlin (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni_flutter:compileReleaseKotlin (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni_flutter:compileReleaseKotlin NO-SOURCE +[ ] Skipping task ':jni_flutter:compileReleaseKotlin' as it has no source files and no previous output files. +[ ] Resolve mutations for :jni_flutter:javaPreCompileRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni_flutter:javaPreCompileRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni_flutter:javaPreCompileRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:javaPreCompileRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:javaPreCompileRelease' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:compileReleaseJavaWithJavac (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni_flutter:compileReleaseJavaWithJavac (Thread[Execution worker Thread 6,5,main]) started. +[ +89 ms] > Task :jni_flutter:compileReleaseJavaWithJavac UP-TO-DATE +[ ] Custom actions are attached to task ':jni_flutter:compileReleaseJavaWithJavac'. +[ ] Caching disabled for task ':jni_flutter:compileReleaseJavaWithJavac' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:compileReleaseJavaWithJavac' as it is up-to-date. +[ ] No compile result for :jni_flutter:compileReleaseJavaWithJavac +[ ] No compile result for :jni_flutter:compileReleaseJavaWithJavac +[ ] No compile result for :jni_flutter:compileReleaseJavaWithJavac +[ ] Resolve mutations for :jni_flutter:bundleLibRuntimeToDirRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni_flutter:bundleLibRuntimeToDirRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni_flutter:bundleLibRuntimeToDirRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:bundleLibRuntimeToDirRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:bundleLibRuntimeToDirRelease' as it is up-to-date. +[ ] work action resolve bundleLibRuntimeToDirRelease (project :jni_flutter) (Thread[Execution worker Thread 6,5,main]) started. +[ ] ClassesDirToClassesTransform (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :jni:bundleLibRuntimeToDirRelease (Thread[Execution worker Thread 4,5,main]) started. +[ ] DexingNoClasspathTransform (Thread[Execution worker Thread 4,5,main]) started. +[ ] :jni:bundleLibRuntimeToDirRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:bundleLibRuntimeToDirRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:bundleLibRuntimeToDirRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:bundleLibRuntimeToDirRelease' as it is up-to-date. +[ ] work action resolve bundleLibRuntimeToDirRelease (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] ClassesDirToClassesTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :app:checkReleaseDuplicateClasses (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:checkReleaseDuplicateClasses (Thread[Execution worker Thread 6,5,main]) started. +[ ] DexingNoClasspathTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Transform bundleLibRuntimeToDirRelease (project :jni_flutter) with DexingNoClasspathTransform +[ ] Caching disabled for DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni_flutter/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease because: +[ ] Build cache is disabled +[ ] Skipping DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni_flutter/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease as it is up-to-date. +[ ] DexingOutputSplitTransform (Thread[Execution worker Thread 4,5,main]) started. +[ ] > Transform bundleLibRuntimeToDirRelease (project :jni) with DexingNoClasspathTransform +[ ] Caching disabled for DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease because: +[ ] Build cache is disabled +[ ] Skipping DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/runtime_library_classes_dir/release/bundleLibRuntimeToDirRelease as it is up-to-date. +[ ] DexingOutputSplitTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:checkReleaseDuplicateClasses UP-TO-DATE +[ ] Caching disabled for task ':app:checkReleaseDuplicateClasses' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:checkReleaseDuplicateClasses' as it is up-to-date. +[ ] Resolve mutations for :app:buildKotlinToolingMetadata (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:buildKotlinToolingMetadata (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:buildKotlinToolingMetadata UP-TO-DATE +[ ] Caching disabled for task ':app:buildKotlinToolingMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:buildKotlinToolingMetadata' as it is up-to-date. +[ ] Resolve mutations for :app:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:checkKotlinGradlePluginConfigurationErrors (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED +[ ] Skipping task ':app:checkKotlinGradlePluginConfigurationErrors' as task onlyIf 'errorDiagnostics are present' is false. +[ ] Resolve mutations for :geolocator_android:bundleLibCompileToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:bundleLibCompileToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :geolocator_android:bundleLibCompileToJarRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:bundleLibCompileToJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:bundleLibCompileToJarRelease' as it is up-to-date. +[ ] work action resolve classes.jar (project :geolocator_android) (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :jni_flutter:bundleLibCompileToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni_flutter:bundleLibCompileToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :jni_flutter:bundleLibCompileToJarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:bundleLibCompileToJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:bundleLibCompileToJarRelease' as it is up-to-date. +[ ] work action resolve classes.jar (project :jni_flutter) (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :app:compileReleaseKotlin (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:compileReleaseKotlin (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:compileReleaseKotlin UP-TO-DATE +[ ] Adding -Xuse-inline-scopes-numbers Kotlin compiler flag for task :app:compileDebugAndroidTestKotlin +[ ] GradleBuildProject.Builder should not be accessed through AnalyticsConfiguratorService after AnalyticsService is created. Analytics information of this build might be incomplete. +[ ] Caching disabled for task ':app:compileReleaseKotlin' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:compileReleaseKotlin' as it is up-to-date. +[ ] Resolve mutations for :app:javaPreCompileRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:javaPreCompileRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:javaPreCompileRelease UP-TO-DATE +[ ] Caching disabled for task ':app:javaPreCompileRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:javaPreCompileRelease' as it is up-to-date. +[ ] Resolve mutations for :app:compileReleaseJavaWithJavac (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:compileReleaseJavaWithJavac (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:compileReleaseJavaWithJavac UP-TO-DATE +[ ] Custom actions are attached to task ':app:compileReleaseJavaWithJavac'. +[ ] Caching disabled for task ':app:compileReleaseJavaWithJavac' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:compileReleaseJavaWithJavac' as it is up-to-date. +[ ] No compile result for :app:compileReleaseJavaWithJavac +[ ] No compile result for :app:compileReleaseJavaWithJavac +[ ] No compile result for :app:compileReleaseJavaWithJavac +[ ] Resolve mutations for :app:dexBuilderRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:dexBuilderRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:dexBuilderRelease UP-TO-DATE +[ ] Caching disabled for task ':app:dexBuilderRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:dexBuilderRelease' as it is up-to-date. +[ ] Resolve mutations for :app:desugarReleaseFileDependencies (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:desugarReleaseFileDependencies (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:desugarReleaseFileDependencies UP-TO-DATE +[ ] Caching disabled for task ':app:desugarReleaseFileDependencies' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:desugarReleaseFileDependencies' as it is up-to-date. +[ ] Resolve mutations for :app:mergeReleaseStartupProfile (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:mergeReleaseStartupProfile (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:mergeReleaseStartupProfile UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseStartupProfile' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':app:mergeReleaseStartupProfile' as it is up-to-date. +[ ] Resolve mutations for :app:mergeExtDexRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:mergeExtDexRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:mergeExtDexRelease UP-TO-DATE +[ ] Caching disabled for task ':app:mergeExtDexRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:mergeExtDexRelease' as it is up-to-date. +[ ] Resolve mutations for :app:mergeDexRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:mergeDexRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:mergeDexRelease UP-TO-DATE +[ ] Caching disabled for task ':app:mergeDexRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:mergeDexRelease' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:bundleLibRuntimeToJarRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:bundleLibRuntimeToJarRelease (Thread[Execution worker Thread 7,5,main]) started. +[ ] > Task :geolocator_android:bundleLibRuntimeToJarRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:bundleLibRuntimeToJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:bundleLibRuntimeToJarRelease' as it is up-to-date. +[ ] work action resolve classes.jar (project :geolocator_android) (Thread[Execution worker Thread 7,5,main]) started. +[ ] IdentityTransform (Thread[Execution worker Thread 7,5,main]) started. +[ ] Resolve mutations for :jni_flutter:bundleLibRuntimeToJarRelease (Thread[Execution worker Thread 2,5,main]) started. +[ ] DexingNoClasspathTransform (Thread[Execution worker Thread 7,5,main]) started. +[ ] :jni_flutter:bundleLibRuntimeToJarRelease (Thread[Execution worker Thread 2,5,main]) started. +[ ] > Task :jni_flutter:bundleLibRuntimeToJarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:bundleLibRuntimeToJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:bundleLibRuntimeToJarRelease' as it is up-to-date. +[ ] work action resolve classes.jar (project :jni_flutter) (Thread[Execution worker Thread 2,5,main]) started. +[ ] IdentityTransform (Thread[Execution worker Thread 2,5,main]) started. +[ ] Resolve mutations for :jni:bundleLibRuntimeToJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] DexingNoClasspathTransform (Thread[Execution worker Thread 2,5,main]) started. +[ ] > Transform classes.jar (project :geolocator_android) with DexingNoClasspathTransform +[ ] Caching disabled for DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/geolocator_android/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar because: +[ ] Build cache is disabled +[ ] Skipping DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/geolocator_android/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar as it is up-to-date. +[ ] :jni:bundleLibRuntimeToJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] DexingOutputSplitTransform (Thread[Execution worker Thread 7,5,main]) started. +[ ] > Transform classes.jar (project :jni_flutter) with DexingNoClasspathTransform +[ ] Caching disabled for DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni_flutter/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar because: +[ ] Build cache is disabled +[ ] Skipping DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni_flutter/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar as it is up-to-date. +[ ] > Task :jni:bundleLibRuntimeToJarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:bundleLibRuntimeToJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:bundleLibRuntimeToJarRelease' as it is up-to-date. +[ ] DexingOutputSplitTransform (Thread[Execution worker Thread 2,5,main]) started. +[ ] work action resolve classes.jar (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] IdentityTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :app:processReleaseJavaRes (Thread[Execution worker Thread 6,5,main]) started. +[ ] DexingNoClasspathTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:processReleaseJavaRes (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Transform classes.jar (project :jni) with DexingNoClasspathTransform +[ ] Caching disabled for DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar because: +[ ] Build cache is disabled +[ ] Skipping DexingNoClasspathTransform: /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/runtime_library_classes_jar/release/bundleLibRuntimeToJarRelease/classes.jar as it is up-to-date. +[ ] DexingOutputSplitTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:processReleaseJavaRes UP-TO-DATE +[ ] file or directory '/Users/anbarasu/Downloads/doormile_flutter/android/app/src/main/resources', not found +[ ] file or directory '/Users/anbarasu/Downloads/doormile_flutter/android/app/src/release/resources', not found +[ ] file or directory '/Users/anbarasu/Downloads/doormile_flutter/android/app/src/main/resources', not found +[ ] file or directory '/Users/anbarasu/Downloads/doormile_flutter/android/app/src/release/resources', not found +[ ] Caching disabled for task ':app:processReleaseJavaRes' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:processReleaseJavaRes' as it is up-to-date. +[ ] Resolve mutations for :app:mergeReleaseGlobalSynthetics (Thread[Execution worker Thread 6,5,main]) started. +[ ] :app:mergeReleaseGlobalSynthetics (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :app:mergeReleaseGlobalSynthetics UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseGlobalSynthetics' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:mergeReleaseGlobalSynthetics' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:processReleaseJavaRes (Thread[Execution worker Thread 6,5,main]) started. +[ ] :geolocator_android:processReleaseJavaRes (Thread[Execution worker Thread 6,5,main]) started. +[ ] > Task :geolocator_android:processReleaseJavaRes NO-SOURCE +[ ] file or directory '/Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/android/src/main/resources', not found +[ ] file or directory '/Users/anbarasu/.pub-cache/hosted/pub.dev/geolocator_android-4.6.2/android/src/release/resources', not found +[ ] Skipping task ':geolocator_android:processReleaseJavaRes' as it has no source files and no previous output files. +[ ] work action resolve out (project :geolocator_android) (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :jni:processReleaseJavaRes (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:processReleaseJavaRes (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:processReleaseJavaRes NO-SOURCE +[ ] file or directory '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/src/main/resources', not found +[ ] file or directory '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/src/release/resources', not found +[ ] Skipping task ':jni:processReleaseJavaRes' as it has no source files and no previous output files. +[ ] work action resolve out (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:processReleaseJavaRes (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:processReleaseJavaRes (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:processReleaseJavaRes NO-SOURCE +[ ] file or directory '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/android/src/main/resources', not found +[ ] file or directory '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/android/src/release/resources', not found +[ ] Skipping task ':jni_flutter:processReleaseJavaRes' as it has no source files and no previous output files. +[ ] work action resolve out (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :app:mergeReleaseJavaResource (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:mergeReleaseJavaResource (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:mergeReleaseJavaResource UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseJavaResource' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:mergeReleaseJavaResource' as it is up-to-date. +[ ] Resolve mutations for :app:configureCMakeRelease[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:configureCMakeRelease[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:configureCMakeRelease[arm64-v8a] +[ ] Caching disabled for task ':app:configureCMakeRelease[arm64-v8a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:configureCMakeRelease[arm64-v8a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: SDK Manager found the following installed packages: build-tools;33.0.1 build-tools;34.0.0 build-tools;35.0.0 build-tools;36.0.0 cmake;3.22.1 cmdline-tools;latest emulator ndk;25.1.8937393 ndk;26.3.11579264 ndk;27.0.12077973 ndk;28.2.13676358 platform-tools platforms;android-13 platforms;android-20 platforms;android-30 platforms;android-31 platforms;android-33 platforms;android-34 platforms;android-35 platforms;android-35-ext14 platforms;android-36 platforms;android-36-ext18 sources;android-20 sources;android-33 sources;android-36 system-images;android-36;google_apis_playstore;arm64-v8a system-images;android-36;google_apis_playstore_ps16k;arm64-v8a +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|arm64-v8a : Start JSON generation. Platform version: 24 min SDK version: arm64-v8a +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|arm64-v8a : JSON '/Users/anbarasu/Downloads/doormile_flutter/build/.cxx/release/11545f15/arm64-v8a/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|arm64-v8a : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|arm64-v8a : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/cxx/release/11545f15/logs/arm64-v8a/build_model.json +[ ] Resolve mutations for :app:buildCMakeRelease[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:buildCMakeRelease[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:buildCMakeRelease[arm64-v8a] +[ ] Caching disabled for task ':app:buildCMakeRelease[arm64-v8a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:buildCMakeRelease[arm64-v8a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: no libraries +[ ] Resolve mutations for :app:configureCMakeRelease[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:configureCMakeRelease[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:configureCMakeRelease[armeabi-v7a] +[ ] Caching disabled for task ':app:configureCMakeRelease[armeabi-v7a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:configureCMakeRelease[armeabi-v7a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|armeabi-v7a : Start JSON generation. Platform version: 24 min SDK version: armeabi-v7a +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|armeabi-v7a : JSON '/Users/anbarasu/Downloads/doormile_flutter/build/.cxx/release/11545f15/armeabi-v7a/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|armeabi-v7a : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|armeabi-v7a : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/cxx/release/11545f15/logs/armeabi-v7a/build_model.json +[ ] Resolve mutations for :app:buildCMakeRelease[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:buildCMakeRelease[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:buildCMakeRelease[armeabi-v7a] +[ ] Caching disabled for task ':app:buildCMakeRelease[armeabi-v7a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:buildCMakeRelease[armeabi-v7a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: no libraries +[ ] Resolve mutations for :app:configureCMakeRelease[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:configureCMakeRelease[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:configureCMakeRelease[x86_64] +[ ] Caching disabled for task ':app:configureCMakeRelease[x86_64]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:configureCMakeRelease[x86_64]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|x86_64 : Start JSON generation. Platform version: 24 min SDK version: x86_64 +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|x86_64 : JSON '/Users/anbarasu/Downloads/doormile_flutter/build/.cxx/release/11545f15/x86_64/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|x86_64 : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/flutter/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt release|x86_64 : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/app/intermediates/cxx/release/11545f15/logs/x86_64/build_model.json +[ ] Resolve mutations for :app:buildCMakeRelease[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:buildCMakeRelease[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:buildCMakeRelease[x86_64] +[ ] Caching disabled for task ':app:buildCMakeRelease[x86_64]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:buildCMakeRelease[x86_64]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: no libraries +[ ] Resolve mutations for :app:copyJniLibsflutterBuildRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:copyJniLibsflutterBuildRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:copyJniLibsflutterBuildRelease UP-TO-DATE +[ ] Caching disabled for task ':app:copyJniLibsflutterBuildRelease' because: +[ ] Build cache is disabled +[ ] Not worth caching +[ ] Skipping task ':app:copyJniLibsflutterBuildRelease' as it is up-to-date. +[ ] Resolve mutations for :app:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:mergeReleaseJniLibFolders UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseJniLibFolders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':app:mergeReleaseJniLibFolders' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseJniLibFolders UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:mergeReleaseJniLibFolders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:mergeReleaseJniLibFolders' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseNativeLibs NO-SOURCE +[ ] Skipping task ':geolocator_android:mergeReleaseNativeLibs' as it has no source files and no previous output files. +[ ] Resolve mutations for :geolocator_android:copyReleaseJniLibsProjectOnly (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:copyReleaseJniLibsProjectOnly (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:copyReleaseJniLibsProjectOnly UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:copyReleaseJniLibsProjectOnly' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:copyReleaseJniLibsProjectOnly' as it is up-to-date. +[ ] work action resolve jni (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:configureCMakeRelWithDebInfo[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:configureCMakeRelWithDebInfo[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:configureCMakeRelWithDebInfo[arm64-v8a] +[ ] Caching disabled for task ':jni:configureCMakeRelWithDebInfo[arm64-v8a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:configureCMakeRelWithDebInfo[arm64-v8a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|arm64-v8a : Start JSON generation. Platform version: 21 min SDK version: arm64-v8a +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|arm64-v8a : JSON '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/arm64-v8a/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|arm64-v8a : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|arm64-v8a : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/cxx/RelWithDebInfo/216c5f6t/logs/arm64-v8a/build_model.json +[ ] Resolve mutations for :jni:buildCMakeRelWithDebInfo[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:buildCMakeRelWithDebInfo[arm64-v8a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:buildCMakeRelWithDebInfo[arm64-v8a] +[ ] Caching disabled for task ':jni:buildCMakeRelWithDebInfo[arm64-v8a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:buildCMakeRelWithDebInfo[arm64-v8a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: evaluate library jni (arm64-v8a) +[ ] C/C++: building target library jni because no targets are specified. +[ ] C/C++: about to build targets jni +[ ] C/C++: Build jni +[ ] C/C++: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja \ +[ ] -C \ +[ ] /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/arm64-v8a \ +[ ] jni +[ ] Starting process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja''. Working directory: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android Command: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja -C /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/arm64-v8a jni +[ ] Successfully started process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja'' +[ ] C/C++: Received process result: 0 +[ ] C/C++: check expected build outputs +[ ] C/C++: build complete +[ ] Resolve mutations for :jni:configureCMakeRelWithDebInfo[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:configureCMakeRelWithDebInfo[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:configureCMakeRelWithDebInfo[armeabi-v7a] +[ ] Caching disabled for task ':jni:configureCMakeRelWithDebInfo[armeabi-v7a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:configureCMakeRelWithDebInfo[armeabi-v7a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|armeabi-v7a : Start JSON generation. Platform version: 21 min SDK version: armeabi-v7a +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|armeabi-v7a : JSON '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/armeabi-v7a/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|armeabi-v7a : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|armeabi-v7a : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/cxx/RelWithDebInfo/216c5f6t/logs/armeabi-v7a/build_model.json +[ ] Resolve mutations for :jni:buildCMakeRelWithDebInfo[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:buildCMakeRelWithDebInfo[armeabi-v7a] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:buildCMakeRelWithDebInfo[armeabi-v7a] +[ ] Caching disabled for task ':jni:buildCMakeRelWithDebInfo[armeabi-v7a]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:buildCMakeRelWithDebInfo[armeabi-v7a]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: evaluate library jni (armeabi-v7a) +[ ] C/C++: building target library jni because no targets are specified. +[ ] C/C++: about to build targets jni +[ ] C/C++: Build jni +[ ] C/C++: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja \ +[ ] -C \ +[ ] /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/armeabi-v7a \ +[ ] jni +[ ] Starting process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja''. Working directory: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android Command: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja -C /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/armeabi-v7a jni +[ ] Successfully started process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja'' +[ ] C/C++: Received process result: 0 +[ ] C/C++: check expected build outputs +[ ] C/C++: build complete +[ ] Resolve mutations for :jni:configureCMakeRelWithDebInfo[x86] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:configureCMakeRelWithDebInfo[x86] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:configureCMakeRelWithDebInfo[x86] +[ ] Caching disabled for task ':jni:configureCMakeRelWithDebInfo[x86]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:configureCMakeRelWithDebInfo[x86]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86 : Start JSON generation. Platform version: 21 min SDK version: x86 +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86 : JSON '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/x86/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86 : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86 : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/cxx/RelWithDebInfo/216c5f6t/logs/x86/build_model.json +[ ] Resolve mutations for :jni:buildCMakeRelWithDebInfo[x86] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:buildCMakeRelWithDebInfo[x86] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:buildCMakeRelWithDebInfo[x86] +[ ] Caching disabled for task ':jni:buildCMakeRelWithDebInfo[x86]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:buildCMakeRelWithDebInfo[x86]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: evaluate library jni (x86) +[ ] C/C++: building target library jni because no targets are specified. +[ ] C/C++: about to build targets jni +[ ] C/C++: Build jni +[ ] C/C++: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja \ +[ ] -C \ +[ ] /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/x86 \ +[ ] jni +[ ] Starting process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja''. Working directory: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android Command: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja -C /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/x86 jni +[ ] Successfully started process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja'' +[ ] C/C++: Received process result: 0 +[ ] C/C++: check expected build outputs +[ ] C/C++: build complete +[ ] Resolve mutations for :jni:configureCMakeRelWithDebInfo[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:configureCMakeRelWithDebInfo[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:configureCMakeRelWithDebInfo[x86_64] +[ ] Caching disabled for task ':jni:configureCMakeRelWithDebInfo[x86_64]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:configureCMakeRelWithDebInfo[x86_64]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86_64 : Start JSON generation. Platform version: 21 min SDK version: x86_64 +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86_64 : JSON '/Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/x86_64/android_gradle_build.json' was up-to-date +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86_64 : JSON generation completed without problems +[ ] C/C++: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/src/CMakeLists.txt release|x86_64 : Writing build model to /Users/anbarasu/Downloads/doormile_flutter/build/jni/intermediates/cxx/RelWithDebInfo/216c5f6t/logs/x86_64/build_model.json +[ ] Resolve mutations for :jni:buildCMakeRelWithDebInfo[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:buildCMakeRelWithDebInfo[x86_64] (Thread[Execution worker Thread 5,5,main]) started. +[ +86 ms] > Task :jni:buildCMakeRelWithDebInfo[x86_64] +[ ] Caching disabled for task ':jni:buildCMakeRelWithDebInfo[x86_64]' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':jni:buildCMakeRelWithDebInfo[x86_64]' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] C/C++: starting build +[ ] C/C++: reading expected JSONs +[ ] C/C++: done reading expected JSONs +[ ] C/C++: executing build commands for targets that produce .so files or executables +[ ] C/C++: evaluate miniconfig +[ ] C/C++: evaluate library jni (x86_64) +[ ] C/C++: building target library jni because no targets are specified. +[ ] C/C++: about to build targets jni +[ ] C/C++: Build jni +[ ] C/C++: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja \ +[ ] -C \ +[ ] /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/x86_64 \ +[ ] jni +[ ] Starting process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja''. Working directory: /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android Command: /Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja -C /Users/anbarasu/.pub-cache/hosted/pub.dev/jni-1.0.0/android/.cxx/RelWithDebInfo/216c5f6t/x86_64 jni +[ ] Successfully started process 'command '/Users/anbarasu/Library/Android/sdk/cmake/3.22.1/bin/ninja'' +[ ] C/C++: Received process result: 0 +[ ] C/C++: check expected build outputs +[ ] C/C++: build complete +[ ] Resolve mutations for :jni:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:mergeReleaseJniLibFolders UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseJniLibFolders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:mergeReleaseJniLibFolders' as it is up-to-date. +[ ] Resolve mutations for :jni:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:mergeReleaseNativeLibs UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseNativeLibs' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:mergeReleaseNativeLibs' as it is up-to-date. +[ ] Resolve mutations for :jni:copyReleaseJniLibsProjectOnly (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:copyReleaseJniLibsProjectOnly (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:copyReleaseJniLibsProjectOnly UP-TO-DATE +[ ] Caching disabled for task ':jni:copyReleaseJniLibsProjectOnly' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:copyReleaseJniLibsProjectOnly' as it is up-to-date. +[ ] work action resolve jni (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni_flutter:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:mergeReleaseJniLibFolders (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseJniLibFolders UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:mergeReleaseJniLibFolders' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:mergeReleaseJniLibFolders' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseNativeLibs NO-SOURCE +[ ] Skipping task ':jni_flutter:mergeReleaseNativeLibs' as it has no source files and no previous output files. +[ ] Resolve mutations for :jni_flutter:copyReleaseJniLibsProjectOnly (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:copyReleaseJniLibsProjectOnly (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:copyReleaseJniLibsProjectOnly UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:copyReleaseJniLibsProjectOnly' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:copyReleaseJniLibsProjectOnly' as it is up-to-date. +[ ] work action resolve jni (project :jni_flutter) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :app:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:mergeReleaseNativeLibs (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:mergeReleaseNativeLibs UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseNativeLibs' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:mergeReleaseNativeLibs' as it is up-to-date. +[ ] Resolve mutations for :app:stripReleaseDebugSymbols (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:stripReleaseDebugSymbols (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:stripReleaseDebugSymbols UP-TO-DATE +[ ] Caching disabled for task ':app:stripReleaseDebugSymbols' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:stripReleaseDebugSymbols' as it is up-to-date. +[ ] Resolve mutations for :app:writeReleaseAppMetadata (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:writeReleaseAppMetadata (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:writeReleaseAppMetadata UP-TO-DATE +[ ] Caching disabled for task ':app:writeReleaseAppMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:writeReleaseAppMetadata' as it is up-to-date. +[ ] Resolve mutations for :app:buildReleasePreBundle (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:buildReleasePreBundle (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:buildReleasePreBundle UP-TO-DATE +[ ] Caching disabled for task ':app:buildReleasePreBundle' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:buildReleasePreBundle' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:prepareReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:prepareReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:prepareReleaseArtProfile UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:prepareReleaseArtProfile' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:prepareReleaseArtProfile' as it is up-to-date. +[ ] work action resolve baseline-prof.txt (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:prepareReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:prepareReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:prepareReleaseArtProfile UP-TO-DATE +[ ] Caching disabled for task ':jni:prepareReleaseArtProfile' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:prepareReleaseArtProfile' as it is up-to-date. +[ ] work action resolve baseline-prof.txt (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni_flutter:prepareReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:prepareReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:prepareReleaseArtProfile UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:prepareReleaseArtProfile' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:prepareReleaseArtProfile' as it is up-to-date. +[ ] work action resolve baseline-prof.txt (project :jni_flutter) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :app:mergeReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:mergeReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:mergeReleaseArtProfile UP-TO-DATE +[ ] Caching disabled for task ':app:mergeReleaseArtProfile' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':app:mergeReleaseArtProfile' as it is up-to-date. +[ ] Resolve mutations for :app:compileReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:compileReleaseArtProfile (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:compileReleaseArtProfile UP-TO-DATE +[ ] Caching disabled for task ':app:compileReleaseArtProfile' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:compileReleaseArtProfile' as it is up-to-date. +[ ] Resolve mutations for :app:collectReleaseDependencies (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:collectReleaseDependencies (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:collectReleaseDependencies UP-TO-DATE +[ ] Caching disabled for task ':app:collectReleaseDependencies' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:collectReleaseDependencies' as it is up-to-date. +[ ] Resolve mutations for :app:configureReleaseDependencies (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:configureReleaseDependencies (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:configureReleaseDependencies UP-TO-DATE +[ ] Caching disabled for task ':app:configureReleaseDependencies' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:configureReleaseDependencies' as it is up-to-date. +[ ] Resolve mutations for :app:extractReleaseNativeSymbolTables (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:extractReleaseNativeSymbolTables (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:extractReleaseNativeSymbolTables UP-TO-DATE +[ ] Caching disabled for task ':app:extractReleaseNativeSymbolTables' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:extractReleaseNativeSymbolTables' as it is up-to-date. +[ ] Resolve mutations for :app:extractProguardFiles (Thread[Execution worker Thread 5,5,main]) started. +[ ] :app:extractProguardFiles (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :app:extractProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':app:extractProguardFiles' because: +[ ] Build cache is disabled +[ ] Fast task +[ ] Skipping task ':app:extractProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:createFullJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:createFullJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:createFullJarRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:createFullJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:createFullJarRelease' as it is up-to-date. +[ ] work action resolve full.jar (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve full.jar (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :geolocator_android:extractProguardFiles (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:extractProguardFiles (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:extractProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:extractProguardFiles' because: +[ ] Build cache is disabled +[ ] Fast task +[ ] Skipping task ':geolocator_android:extractProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:generateReleaseLintModel (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:generateReleaseLintModel (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:generateReleaseLintModel UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:generateReleaseLintModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:generateReleaseLintModel' as it is up-to-date. +[ ] work action resolve generateReleaseLintModel (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve generateReleaseLintModel (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :geolocator_android:prepareLintJarForPublish (Thread[Execution worker Thread 5,5,main]) started. +[ ] :geolocator_android:prepareLintJarForPublish (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :geolocator_android:prepareLintJarForPublish UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:prepareLintJarForPublish' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:prepareLintJarForPublish' as it is up-to-date. +[ ] work action resolve lint.jar (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve lint.jar (project :geolocator_android) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:createFullJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:createFullJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:createFullJarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:createFullJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:createFullJarRelease' as it is up-to-date. +[ +1 ms] work action resolve full.jar (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve full.jar (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:extractProguardFiles (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:extractProguardFiles (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:extractProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':jni:extractProguardFiles' because: +[ ] Build cache is disabled +[ ] Fast task +[ ] Skipping task ':jni:extractProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :jni:generateReleaseLintModel (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:generateReleaseLintModel (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:generateReleaseLintModel UP-TO-DATE +[ ] Caching disabled for task ':jni:generateReleaseLintModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:generateReleaseLintModel' as it is up-to-date. +[ ] work action resolve generateReleaseLintModel (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve generateReleaseLintModel (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni:prepareLintJarForPublish (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni:prepareLintJarForPublish (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni:prepareLintJarForPublish UP-TO-DATE +[ ] Caching disabled for task ':jni:prepareLintJarForPublish' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:prepareLintJarForPublish' as it is up-to-date. +[ ] work action resolve lint.jar (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve lint.jar (project :jni) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni_flutter:createFullJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] :jni_flutter:createFullJarRelease (Thread[Execution worker Thread 5,5,main]) started. +[ ] > Task :jni_flutter:createFullJarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:createFullJarRelease' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:createFullJarRelease' as it is up-to-date. +[ ] work action resolve full.jar (project :jni_flutter) (Thread[Execution worker Thread 5,5,main]) started. +[ ] work action resolve full.jar (project :jni_flutter) (Thread[Execution worker Thread 5,5,main]) started. +[ ] Resolve mutations for :jni_flutter:extractProguardFiles (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:extractProguardFiles (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:extractProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:extractProguardFiles' because: +[ ] Build cache is disabled +[ ] Fast task +[ ] Skipping task ':jni_flutter:extractProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:generateReleaseLintModel (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:generateReleaseLintModel (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:generateReleaseLintModel UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:generateReleaseLintModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:generateReleaseLintModel' as it is up-to-date. +[ ] work action resolve generateReleaseLintModel (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] work action resolve generateReleaseLintModel (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:prepareLintJarForPublish (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:prepareLintJarForPublish (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni_flutter:prepareLintJarForPublish UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:prepareLintJarForPublish' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:prepareLintJarForPublish' as it is up-to-date. +[ ] work action resolve lint.jar (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] work action resolve lint.jar (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :app:generateReleaseLintVitalReportModel (Thread[Execution worker Thread 9,5,main]) started. +[ ] :app:generateReleaseLintVitalReportModel (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :app:generateReleaseLintVitalReportModel UP-TO-DATE +[ ] Caching disabled for task ':app:generateReleaseLintVitalReportModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:generateReleaseLintVitalReportModel' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:stripReleaseDebugSymbols (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:stripReleaseDebugSymbols (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:stripReleaseDebugSymbols NO-SOURCE +[ ] Skipping task ':geolocator_android:stripReleaseDebugSymbols' as it has no source files and no previous output files. +[ ] Resolve mutations for :geolocator_android:copyReleaseJniLibsProjectAndLocalJars (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:copyReleaseJniLibsProjectAndLocalJars (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:copyReleaseJniLibsProjectAndLocalJars UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:copyReleaseJniLibsProjectAndLocalJars' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:copyReleaseJniLibsProjectAndLocalJars' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:extractDeepLinksForAarRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:extractDeepLinksForAarRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:extractDeepLinksForAarRelease UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:extractDeepLinksForAarRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:extractDeepLinksForAarRelease' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:extractReleaseAnnotations (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:extractReleaseAnnotations (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:extractReleaseAnnotations UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:extractReleaseAnnotations' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:extractReleaseAnnotations' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:mergeReleaseGeneratedProguardFiles (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:mergeReleaseGeneratedProguardFiles (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseGeneratedProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:mergeReleaseGeneratedProguardFiles' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:mergeReleaseGeneratedProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:mergeReleaseConsumerProguardFiles (Thread[included builds,5,main]) started. +[ ] :geolocator_android:mergeReleaseConsumerProguardFiles (Thread[included builds,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseConsumerProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:mergeReleaseConsumerProguardFiles' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':geolocator_android:mergeReleaseConsumerProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:mergeReleaseJavaResource (Thread[included builds,5,main]) started. +[ ] :geolocator_android:mergeReleaseJavaResource (Thread[included builds,5,main]) started. +[ ] > Task :geolocator_android:mergeReleaseJavaResource UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:mergeReleaseJavaResource' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:mergeReleaseJavaResource' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:syncReleaseLibJars (Thread[included builds,5,main]) started. +[ ] :geolocator_android:syncReleaseLibJars (Thread[included builds,5,main]) started. +[ ] > Task :geolocator_android:syncReleaseLibJars UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:syncReleaseLibJars' because: +[ ] Build cache is disabled +[ ] Skipping task ':geolocator_android:syncReleaseLibJars' as it is up-to-date. +[ ] Resolve mutations for :geolocator_android:bundleReleaseLocalLintAar (Thread[included builds,5,main]) started. +[ ] :geolocator_android:bundleReleaseLocalLintAar (Thread[included builds,5,main]) started. +[ ] > Task :geolocator_android:bundleReleaseLocalLintAar UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:bundleReleaseLocalLintAar' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:bundleReleaseLocalLintAar' as it is up-to-date. +[ ] work action resolve out.aar (project :geolocator_android) (Thread[included builds,5,main]) started. +[ ] work action resolve out.aar (project :geolocator_android) (Thread[included builds,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :jni_flutter:stripReleaseDebugSymbols (Thread[included builds,5,main]) started. +[ ] :jni_flutter:stripReleaseDebugSymbols (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:stripReleaseDebugSymbols NO-SOURCE +[ ] Skipping task ':jni_flutter:stripReleaseDebugSymbols' as it has no source files and no previous output files. +[ ] Resolve mutations for :jni_flutter:copyReleaseJniLibsProjectAndLocalJars (Thread[included builds,5,main]) started. +[ ] :jni_flutter:copyReleaseJniLibsProjectAndLocalJars (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:copyReleaseJniLibsProjectAndLocalJars UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:copyReleaseJniLibsProjectAndLocalJars' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:copyReleaseJniLibsProjectAndLocalJars' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:extractDeepLinksForAarRelease (Thread[included builds,5,main]) started. +[ ] :jni_flutter:extractDeepLinksForAarRelease (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:extractDeepLinksForAarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:extractDeepLinksForAarRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:extractDeepLinksForAarRelease' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:extractReleaseAnnotations (Thread[included builds,5,main]) started. +[ ] :jni_flutter:extractReleaseAnnotations (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:extractReleaseAnnotations UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:extractReleaseAnnotations' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:extractReleaseAnnotations' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:mergeReleaseGeneratedProguardFiles (Thread[included builds,5,main]) started. +[ ] :jni_flutter:mergeReleaseGeneratedProguardFiles (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseGeneratedProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:mergeReleaseGeneratedProguardFiles' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:mergeReleaseGeneratedProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:mergeReleaseConsumerProguardFiles (Thread[included builds,5,main]) started. +[ ] :jni_flutter:mergeReleaseConsumerProguardFiles (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseConsumerProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:mergeReleaseConsumerProguardFiles' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni_flutter:mergeReleaseConsumerProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:mergeReleaseJavaResource (Thread[included builds,5,main]) started. +[ ] :jni_flutter:mergeReleaseJavaResource (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:mergeReleaseJavaResource UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:mergeReleaseJavaResource' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:mergeReleaseJavaResource' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:syncReleaseLibJars (Thread[included builds,5,main]) started. +[ ] :jni_flutter:syncReleaseLibJars (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:syncReleaseLibJars UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:syncReleaseLibJars' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni_flutter:syncReleaseLibJars' as it is up-to-date. +[ ] Resolve mutations for :jni_flutter:bundleReleaseLocalLintAar (Thread[included builds,5,main]) started. +[ ] :jni_flutter:bundleReleaseLocalLintAar (Thread[included builds,5,main]) started. +[ ] > Task :jni_flutter:bundleReleaseLocalLintAar UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:bundleReleaseLocalLintAar' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:bundleReleaseLocalLintAar' as it is up-to-date. +[ ] work action resolve out.aar (project :jni_flutter) (Thread[included builds,5,main]) started. +[ ] work action resolve out.aar (project :jni_flutter) (Thread[included builds,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker Thread 2,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker Thread 7,5,main]) started. +[ ] Resolve mutations for :jni:stripReleaseDebugSymbols (Thread[included builds,5,main]) started. +[ ] :jni:stripReleaseDebugSymbols (Thread[included builds,5,main]) started. +[ ] > Task :jni:stripReleaseDebugSymbols UP-TO-DATE +[ ] Caching disabled for task ':jni:stripReleaseDebugSymbols' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:stripReleaseDebugSymbols' as it is up-to-date. +[ ] Resolve mutations for :jni:copyReleaseJniLibsProjectAndLocalJars (Thread[included builds,5,main]) started. +[ ] :jni:copyReleaseJniLibsProjectAndLocalJars (Thread[included builds,5,main]) started. +[ ] > Task :jni:copyReleaseJniLibsProjectAndLocalJars UP-TO-DATE +[ ] Caching disabled for task ':jni:copyReleaseJniLibsProjectAndLocalJars' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:copyReleaseJniLibsProjectAndLocalJars' as it is up-to-date. +[ ] Resolve mutations for :jni:extractDeepLinksForAarRelease (Thread[included builds,5,main]) started. +[ ] :jni:extractDeepLinksForAarRelease (Thread[included builds,5,main]) started. +[ ] > Task :jni:extractDeepLinksForAarRelease UP-TO-DATE +[ ] Caching disabled for task ':jni:extractDeepLinksForAarRelease' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:extractDeepLinksForAarRelease' as it is up-to-date. +[ ] Resolve mutations for :jni:extractReleaseAnnotations (Thread[included builds,5,main]) started. +[ ] :jni:extractReleaseAnnotations (Thread[included builds,5,main]) started. +[ ] > Task :jni:extractReleaseAnnotations UP-TO-DATE +[ ] Caching disabled for task ':jni:extractReleaseAnnotations' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:extractReleaseAnnotations' as it is up-to-date. +[ ] Resolve mutations for :jni:mergeReleaseGeneratedProguardFiles (Thread[included builds,5,main]) started. +[ ] :jni:mergeReleaseGeneratedProguardFiles (Thread[included builds,5,main]) started. +[ ] > Task :jni:mergeReleaseGeneratedProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseGeneratedProguardFiles' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:mergeReleaseGeneratedProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :jni:mergeReleaseConsumerProguardFiles (Thread[included builds,5,main]) started. +[ ] :jni:mergeReleaseConsumerProguardFiles (Thread[included builds,5,main]) started. +[ ] > Task :jni:mergeReleaseConsumerProguardFiles UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseConsumerProguardFiles' because: +[ ] Build cache is disabled +[ ] Simple merging task +[ ] Skipping task ':jni:mergeReleaseConsumerProguardFiles' as it is up-to-date. +[ ] Resolve mutations for :jni:mergeReleaseJavaResource (Thread[included builds,5,main]) started. +[ ] :jni:mergeReleaseJavaResource (Thread[included builds,5,main]) started. +[ ] > Task :jni:mergeReleaseJavaResource UP-TO-DATE +[ ] Caching disabled for task ':jni:mergeReleaseJavaResource' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:mergeReleaseJavaResource' as it is up-to-date. +[ ] Resolve mutations for :jni:syncReleaseLibJars (Thread[included builds,5,main]) started. +[ ] :jni:syncReleaseLibJars (Thread[included builds,5,main]) started. +[ ] > Task :jni:syncReleaseLibJars UP-TO-DATE +[ ] Caching disabled for task ':jni:syncReleaseLibJars' because: +[ ] Build cache is disabled +[ ] Skipping task ':jni:syncReleaseLibJars' as it is up-to-date. +[ ] Resolve mutations for :jni:bundleReleaseLocalLintAar (Thread[included builds,5,main]) started. +[ ] :jni:bundleReleaseLocalLintAar (Thread[included builds,5,main]) started. +[ ] > Task :jni:bundleReleaseLocalLintAar UP-TO-DATE +[ ] Caching disabled for task ':jni:bundleReleaseLocalLintAar' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:bundleReleaseLocalLintAar' as it is up-to-date. +[ ] work action resolve out.aar (project :jni) (Thread[included builds,5,main]) started. +[ ] work action resolve out.aar (project :jni) (Thread[included builds,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker Thread 5,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :geolocator_android:lintVitalAnalyzeRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] ExtractAarTransform (Thread[Execution worker Thread 2,5,main]) started. +[ ] ExtractAarTransform (Thread[included builds,5,main]) started. +[ ] :geolocator_android:lintVitalAnalyzeRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :geolocator_android:writeReleaseLintModelMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] :geolocator_android:writeReleaseLintModelMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :geolocator_android:writeReleaseLintModelMetadata UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:writeReleaseLintModelMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:writeReleaseLintModelMetadata' as it is up-to-date. +[ ] work action resolve lint-model-metadata.properties (project :geolocator_android) (Thread[Execution worker Thread 9,5,main]) started. +[ ] work action resolve lint-model-metadata.properties (project :geolocator_android) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni:lintVitalAnalyzeRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] :jni:lintVitalAnalyzeRelease (Thread[Execution worker Thread 6,5,main]) started. +[ ] Resolve mutations for :jni:writeReleaseLintModelMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni:writeReleaseLintModelMetadata (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:writeReleaseLintModelMetadata UP-TO-DATE +[ ] Caching disabled for task ':jni:writeReleaseLintModelMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:writeReleaseLintModelMetadata' as it is up-to-date. +[ ] work action resolve lint-model-metadata.properties (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] work action resolve lint-model-metadata.properties (project :jni) (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:lintVitalAnalyzeRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] :jni_flutter:lintVitalAnalyzeRelease (Thread[Execution worker Thread 9,5,main]) started. +[ ] Resolve mutations for :jni_flutter:writeReleaseLintModelMetadata (Thread[Execution worker,5,main]) started. +[ ] :jni_flutter:writeReleaseLintModelMetadata (Thread[Execution worker,5,main]) started. +[ ] > Task :jni_flutter:writeReleaseLintModelMetadata UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:writeReleaseLintModelMetadata' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:writeReleaseLintModelMetadata' as it is up-to-date. +[ ] work action resolve lint-model-metadata.properties (project :jni_flutter) (Thread[Execution worker,5,main]) started. +[ ] work action resolve lint-model-metadata.properties (project :jni_flutter) (Thread[Execution worker,5,main]) started. +[ ] Resolve mutations for :geolocator_android:generateReleaseLintVitalModel (Thread[Execution worker,5,main]) started. +[ ] :geolocator_android:generateReleaseLintVitalModel (Thread[Execution worker,5,main]) started. +[ ] > Task :geolocator_android:generateReleaseLintVitalModel UP-TO-DATE +[ ] Caching disabled for task ':geolocator_android:generateReleaseLintVitalModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':geolocator_android:generateReleaseLintVitalModel' as it is up-to-date. +[ ] work action resolve generateReleaseLintVitalModel (project :geolocator_android) (Thread[Execution worker,5,main]) started. +[ ] work action resolve generateReleaseLintVitalModel (project :geolocator_android) (Thread[Execution worker,5,main]) started. +[ ] Resolve mutations for :jni:generateReleaseLintVitalModel (Thread[Execution worker,5,main]) started. +[ ] :jni:generateReleaseLintVitalModel (Thread[Execution worker,5,main]) started. +[ ] > Task :jni:generateReleaseLintVitalModel UP-TO-DATE +[ ] Caching disabled for task ':jni:generateReleaseLintVitalModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni:generateReleaseLintVitalModel' as it is up-to-date. +[ ] work action resolve generateReleaseLintVitalModel (project :jni) (Thread[Execution worker,5,main]) started. +[ ] work action resolve generateReleaseLintVitalModel (project :jni) (Thread[Execution worker,5,main]) started. +[ ] Resolve mutations for :jni_flutter:generateReleaseLintVitalModel (Thread[Execution worker,5,main]) started. +[ ] :jni_flutter:generateReleaseLintVitalModel (Thread[Execution worker,5,main]) started. +[ ] > Task :jni_flutter:generateReleaseLintVitalModel UP-TO-DATE +[ ] Caching disabled for task ':jni_flutter:generateReleaseLintVitalModel' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':jni_flutter:generateReleaseLintVitalModel' as it is up-to-date. +[ ] work action resolve generateReleaseLintVitalModel (project :jni_flutter) (Thread[Execution worker,5,main]) started. +[ ] work action resolve generateReleaseLintVitalModel (project :jni_flutter) (Thread[Execution worker,5,main]) started. +[ ] Resolve mutations for :app:parseReleaseIntegrityConfig (Thread[Execution worker,5,main]) started. +[ ] :app:parseReleaseIntegrityConfig (Thread[Execution worker,5,main]) started. +[ ] > Task :app:parseReleaseIntegrityConfig UP-TO-DATE +[ ] Caching disabled for task ':app:parseReleaseIntegrityConfig' because: +[ ] Build cache is disabled +[ ] Skipping task ':app:parseReleaseIntegrityConfig' as it is up-to-date. +[ ] Resolve mutations for :app:validateSigningRelease (Thread[Execution worker,5,main]) started. +[ ] :app:validateSigningRelease (Thread[Execution worker,5,main]) started. +[ ] > Task :app:validateSigningRelease UP-TO-DATE +[ ] Caching disabled for task ':app:validateSigningRelease' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:validateSigningRelease' as it is up-to-date. +[ +173 ms] > Task :jni_flutter:lintVitalAnalyzeRelease +[ ] Caching disabled for task ':jni_flutter:lintVitalAnalyzeRelease' because: +[ ] Build cache is disabled +[ ] Task ':jni_flutter:lintVitalAnalyzeRelease' is not up-to-date because: +[ ] Value of input property 'printStackTrace' has changed for task ':jni_flutter:lintVitalAnalyzeRelease' +[ ] Android Lint: Reusing lint classloader 31.11.1_true +[ ] work action resolve out (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] work action resolve out (project :jni_flutter) (Thread[Execution worker Thread 9,5,main]) started. +[ ] > Task :jni:lintVitalAnalyzeRelease +[ ] Caching disabled for task ':jni:lintVitalAnalyzeRelease' because: +[ ] Build cache is disabled +[ ] Task ':jni:lintVitalAnalyzeRelease' is not up-to-date because: +[ ] Value of input property 'printStackTrace' has changed for task ':jni:lintVitalAnalyzeRelease' +[ ] Android Lint: Reusing lint classloader 31.11.1_true +[ ] work action resolve out (project :jni) (Thread[Execution worker Thread 6,5,main]) started. +[ ] work action resolve out (project :jni) (Thread[Execution worker Thread 4,5,main]) started. +[ +101 ms] > Task :geolocator_android:lintVitalAnalyzeRelease +[ ] Caching disabled for task ':geolocator_android:lintVitalAnalyzeRelease' because: +[ ] Build cache is disabled +[ ] Task ':geolocator_android:lintVitalAnalyzeRelease' is not up-to-date because: +[ ] Value of input property 'printStackTrace' has changed for task ':geolocator_android:lintVitalAnalyzeRelease' +[ ] Android Lint: Reusing lint classloader 31.11.1_true +[ ] work action resolve out (project :geolocator_android) (Thread[Execution worker Thread 3,5,main]) started. +[ ] work action resolve out (project :geolocator_android) (Thread[Execution worker Thread 3,5,main]) started. +[ ] Resolve mutations for :app:lintVitalAnalyzeRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:lintVitalAnalyzeRelease (Thread[Execution worker Thread 3,5,main]) started. +[ +192 ms] > Task :app:lintVitalAnalyzeRelease +[ ] Caching disabled for task ':app:lintVitalAnalyzeRelease' because: +[ ] Build cache is disabled +[ ] Task ':app:lintVitalAnalyzeRelease' is not up-to-date because: +[ ] Value of input property 'printStackTrace' has changed for task ':app:lintVitalAnalyzeRelease' +[ ] Android Lint: Reusing lint classloader 31.11.1_true +[ ] Resolve mutations for :app:lintVitalReportRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:lintVitalReportRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:lintVitalReportRelease +[ ] Caching disabled for task ':app:lintVitalReportRelease' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:lintVitalReportRelease' is not up-to-date because: +[ ] Value of input property 'printStackTrace' has changed for task ':app:lintVitalReportRelease' +[ ] Android Lint: Reusing lint classloader 31.11.1_true +[ ] Resolve mutations for :app:lintVitalRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:lintVitalRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:lintVitalRelease +[ ] Caching disabled for task ':app:lintVitalRelease' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Task ':app:lintVitalRelease' is not up-to-date because: +[ ] Task.upToDateWhen is false. +[ ] Resolve mutations for :app:packageReleaseBundle (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:packageReleaseBundle (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:packageReleaseBundle UP-TO-DATE +[ ] Caching disabled for task ':app:packageReleaseBundle' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:packageReleaseBundle' as it is up-to-date. +[ ] Resolve mutations for :app:signReleaseBundle (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:signReleaseBundle (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:signReleaseBundle UP-TO-DATE +[ ] Caching disabled for task ':app:signReleaseBundle' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:signReleaseBundle' as it is up-to-date. +[ ] Resolve mutations for :app:produceReleaseBundleIdeListingFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:produceReleaseBundleIdeListingFile (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:produceReleaseBundleIdeListingFile UP-TO-DATE +[ ] Caching disabled for task ':app:produceReleaseBundleIdeListingFile' because: +[ ] Build cache is disabled +[ ] Fast task +[ ] Skipping task ':app:produceReleaseBundleIdeListingFile' as it is up-to-date. +[ ] Resolve mutations for :app:createReleaseBundleListingFileRedirect (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:createReleaseBundleListingFileRedirect (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:createReleaseBundleListingFileRedirect UP-TO-DATE +[ ] Caching disabled for task ':app:createReleaseBundleListingFileRedirect' because: +[ ] Build cache is disabled +[ ] Caching has been disabled for the task +[ ] Skipping task ':app:createReleaseBundleListingFileRedirect' as it is up-to-date. +[ ] Resolve mutations for :app:bundleRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] :app:bundleRelease (Thread[Execution worker Thread 3,5,main]) started. +[ ] > Task :app:bundleRelease UP-TO-DATE +[ ] Skipping task ':app:bundleRelease' as it has no actions. +[ ] Build b0fbb022-5ca9-44e4-94f6-596ae9452722 is closed +[ ] Android Lint: Disposing Uast application environment in lint classloader [31.11.1_true] +[ ] Build 982634eb-17c1-4374-b8bf-069dc6d4837f is closed +[ ] [Incubating] Problems report is available at: file:///Users/anbarasu/Downloads/doormile_flutter/build/reports/problems/problems-report.html +[ ] Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0. +[ ] You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. +[ ] For more on this, please refer to https://docs.gradle.org/8.14/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. +[ ] BUILD SUCCESSFUL in 2s +[ ] 179 actionable tasks: 24 executed, 155 up-to-date +[ ] Watched directory hierarchies: [/Users/anbarasu/flutter/packages/flutter_tools/gradle, /Users/anbarasu/Downloads/doormile_flutter/android] +[ +4 ms] Running Gradle task 'bundleRelease'... (completed in 2,723ms) +[ +7 ms] executing: [/Users/anbarasu/Downloads/doormile_flutter/android/] /Users/anbarasu/Library/Android/sdk/cmdline-tools/latest/bin/apkanalyzer files list /Users/anbarasu/Downloads/doormile_flutter/build/app/outputs/bundle/release/app-release.aab +[ +384 ms] / + /META-INF/ + /META-INF/MANIFEST.MF + /META-INF/UPLOAD.RSA + /META-INF/UPLOAD.SF + /base/ + /base/root/ + /base/root/play-services-tasks.properties + /base/root/play-services-location.properties + /base/root/play-services-basement.properties + /base/root/play-services-base.properties + /base/root/kotlin/ + /base/root/kotlin/reflect/ + /base/root/kotlin/reflect/reflect.kotlin_builtins + /base/root/kotlin/ranges/ + /base/root/kotlin/ranges/ranges.kotlin_builtins + /base/root/kotlin/kotlin.kotlin_builtins + /base/root/kotlin/internal/ + /base/root/kotlin/internal/internal.kotlin_builtins + /base/root/kotlin/coroutines/ + /base/root/kotlin/coroutines/coroutines.kotlin_builtins + /base/root/kotlin/concurrent/ + /base/root/kotlin/concurrent/atomics/ + /base/root/kotlin/concurrent/atomics/atomics.kotlin_builtins + /base/root/kotlin/collections/ + /base/root/kotlin/collections/collections.kotlin_builtins + /base/root/kotlin/annotation/ + /base/root/kotlin/annotation/annotation.kotlin_builtins + /base/root/kotlin-tooling-metadata.json + /base/root/META-INF/ + /base/root/META-INF/versions/ + /base/root/META-INF/versions/9/ + /base/root/META-INF/versions/9/OSGI-INF/ + /base/root/META-INF/versions/9/OSGI-INF/MANIFEST.MF + /base/root/META-INF/version-control-info.textproto + /base/root/META-INF/services/ + /base/root/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory + /base/root/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler + /base/root/META-INF/com/ + /base/root/META-INF/com/android/ + /base/root/META-INF/com/android/build/ + /base/root/META-INF/com/android/build/gradle/ + /base/root/META-INF/com/android/build/gradle/app-metadata.properties + /base/root/DebugProbesKt.bin + /base/resources.pb + /base/res/ + /base/res/mipmap-xxxhdpi-v4/ + /base/res/mipmap-xxxhdpi-v4/launcher_icon.png + /base/res/mipmap-xxxhdpi-v4/ic_launcher.png + /base/res/mipmap-xxhdpi-v4/ + /base/res/mipmap-xxhdpi-v4/launcher_icon.png + /base/res/mipmap-xxhdpi-v4/ic_launcher.png + /base/res/mipmap-xhdpi-v4/ + /base/res/mipmap-xhdpi-v4/launcher_icon.png + /base/res/mipmap-xhdpi-v4/ic_launcher.png + /base/res/mipmap-mdpi-v4/ + /base/res/mipmap-mdpi-v4/launcher_icon.png + /base/res/mipmap-mdpi-v4/ic_launcher.png + /base/res/mipmap-hdpi-v4/ + /base/res/mipmap-hdpi-v4/launcher_icon.png + /base/res/mipmap-hdpi-v4/ic_launcher.png + /base/res/layout/ + /base/res/layout/notification_template_part_time.xml + /base/res/layout/notification_template_part_chronometer.xml + /base/res/layout/ime_secondary_split_test_activity.xml + /base/res/layout/ime_base_split_test_activity.xml + /base/res/layout/custom_dialog.xml + /base/res/layout-v21/ + /base/res/layout-v21/notification_template_icon_group.xml + /base/res/layout-v21/notification_template_custom_big.xml + /base/res/layout-v21/notification_action_tombstone.xml + /base/res/layout-v21/notification_action.xml + /base/res/drawable/ + /base/res/drawable/notification_tile_bg.xml + /base/res/drawable/notification_icon_background.xml + /base/res/drawable/notification_bg_low.xml + /base/res/drawable/notification_bg.xml + /base/res/drawable/common_google_signin_btn_text_light_normal.xml + /base/res/drawable/common_google_signin_btn_text_light_focused.xml + /base/res/drawable/common_google_signin_btn_text_light.xml + /base/res/drawable/common_google_signin_btn_text_disabled.xml + /base/res/drawable/common_google_signin_btn_text_dark_normal.xml + /base/res/drawable/common_google_signin_btn_text_dark_focused.xml + /base/res/drawable/common_google_signin_btn_text_dark.xml + /base/res/drawable/common_google_signin_btn_icon_light_normal.xml + /base/res/drawable/common_google_signin_btn_icon_light_focused.xml + /base/res/drawable/common_google_signin_btn_icon_light.xml + /base/res/drawable/common_google_signin_btn_icon_disabled.xml + /base/res/drawable/common_google_signin_btn_icon_dark_normal.xml + /base/res/drawable/common_google_signin_btn_icon_dark_focused.xml + /base/res/drawable/common_google_signin_btn_icon_dark.xml + /base/res/drawable-xxxhdpi-v4/ + /base/res/drawable-xxxhdpi-v4/ic_call_decline_low.png + /base/res/drawable-xxxhdpi-v4/ic_call_decline.png + /base/res/drawable-xxxhdpi-v4/ic_call_answer_video_low.png + /base/res/drawable-xxxhdpi-v4/ic_call_answer_video.png + /base/res/drawable-xxxhdpi-v4/ic_call_answer_low.png + /base/res/drawable-xxxhdpi-v4/ic_call_answer.png + /base/res/drawable-xxhdpi-v4/ + /base/res/drawable-xxhdpi-v4/ic_call_decline_low.png + /base/res/drawable-xxhdpi-v4/ic_call_decline.png + /base/res/drawable-xxhdpi-v4/ic_call_answer_video_low.png + /base/res/drawable-xxhdpi-v4/ic_call_answer_video.png + /base/res/drawable-xxhdpi-v4/ic_call_answer_low.png + /base/res/drawable-xxhdpi-v4/ic_call_answer.png + /base/res/drawable-xxhdpi-v4/googleg_standard_color_18.png + /base/res/drawable-xxhdpi-v4/googleg_disabled_color_18.png + /base/res/drawable-xxhdpi-v4/common_google_signin_btn_text_light_normal_background.9.png + /base/res/drawable-xxhdpi-v4/common_google_signin_btn_text_dark_normal_background.9.png + /base/res/drawable-xxhdpi-v4/common_google_signin_btn_icon_light_normal_background.9.png + /base/res/drawable-xxhdpi-v4/common_google_signin_btn_icon_dark_normal_background.9.png + /base/res/drawable-xhdpi-v4/ + /base/res/drawable-xhdpi-v4/notify_panel_notification_icon_bg.png + /base/res/drawable-xhdpi-v4/notification_bg_normal_pressed.9.png + /base/res/drawable-xhdpi-v4/notification_bg_normal.9.png + /base/res/drawable-xhdpi-v4/notification_bg_low_pressed.9.png + /base/res/drawable-xhdpi-v4/notification_bg_low_normal.9.png + /base/res/drawable-xhdpi-v4/ic_call_decline_low.png + /base/res/drawable-xhdpi-v4/ic_call_decline.png + /base/res/drawable-xhdpi-v4/ic_call_answer_video_low.png + /base/res/drawable-xhdpi-v4/ic_call_answer_video.png + /base/res/drawable-xhdpi-v4/ic_call_answer_low.png + /base/res/drawable-xhdpi-v4/ic_call_answer.png + /base/res/drawable-xhdpi-v4/googleg_standard_color_18.png + /base/res/drawable-xhdpi-v4/googleg_disabled_color_18.png + /base/res/drawable-xhdpi-v4/common_google_signin_btn_text_light_normal_background.9.png + /base/res/drawable-xhdpi-v4/common_google_signin_btn_text_dark_normal_background.9.png + /base/res/drawable-xhdpi-v4/common_google_signin_btn_icon_light_normal_background.9.png + /base/res/drawable-xhdpi-v4/common_google_signin_btn_icon_dark_normal_background.9.png + /base/res/drawable-xhdpi-v4/common_full_open_on_phone.png + /base/res/drawable-v21/ + /base/res/drawable-v21/notification_action_background.xml + /base/res/drawable-v21/launch_background.xml + /base/res/drawable-mdpi-v4/ + /base/res/drawable-mdpi-v4/notify_panel_notification_icon_bg.png + /base/res/drawable-mdpi-v4/notification_bg_normal_pressed.9.png + /base/res/drawable-mdpi-v4/notification_bg_normal.9.png + /base/res/drawable-mdpi-v4/notification_bg_low_pressed.9.png + /base/res/drawable-mdpi-v4/notification_bg_low_normal.9.png + /base/res/drawable-mdpi-v4/ic_call_decline_low.png + /base/res/drawable-mdpi-v4/ic_call_decline.png + /base/res/drawable-mdpi-v4/ic_call_answer_video_low.png + /base/res/drawable-mdpi-v4/ic_call_answer_video.png + /base/res/drawable-mdpi-v4/ic_call_answer_low.png + /base/res/drawable-mdpi-v4/ic_call_answer.png + /base/res/drawable-mdpi-v4/googleg_standard_color_18.png + /base/res/drawable-mdpi-v4/googleg_disabled_color_18.png + /base/res/drawable-mdpi-v4/common_google_signin_btn_text_light_normal_background.9.png + /base/res/drawable-mdpi-v4/common_google_signin_btn_text_dark_normal_background.9.png + /base/res/drawable-mdpi-v4/common_google_signin_btn_icon_light_normal_background.9.png + /base/res/drawable-mdpi-v4/common_google_signin_btn_icon_dark_normal_background.9.png + /base/res/drawable-ldpi-v4/ + /base/res/drawable-ldpi-v4/ic_call_decline_low.png + /base/res/drawable-ldpi-v4/ic_call_decline.png + /base/res/drawable-ldpi-v4/ic_call_answer_video_low.png + /base/res/drawable-ldpi-v4/ic_call_answer_video.png + /base/res/drawable-ldpi-v4/ic_call_answer_low.png + /base/res/drawable-ldpi-v4/ic_call_answer.png + /base/res/drawable-hdpi-v4/ + /base/res/drawable-hdpi-v4/notify_panel_notification_icon_bg.png + /base/res/drawable-hdpi-v4/notification_oversize_large_icon_bg.png + /base/res/drawable-hdpi-v4/notification_bg_normal_pressed.9.png + /base/res/drawable-hdpi-v4/notification_bg_normal.9.png + /base/res/drawable-hdpi-v4/notification_bg_low_pressed.9.png + /base/res/drawable-hdpi-v4/notification_bg_low_normal.9.png + /base/res/drawable-hdpi-v4/ic_call_decline_low.png + /base/res/drawable-hdpi-v4/ic_call_decline.png + /base/res/drawable-hdpi-v4/ic_call_answer_video_low.png + /base/res/drawable-hdpi-v4/ic_call_answer_video.png + /base/res/drawable-hdpi-v4/ic_call_answer_low.png + /base/res/drawable-hdpi-v4/ic_call_answer.png + /base/res/drawable-hdpi-v4/googleg_standard_color_18.png + /base/res/drawable-hdpi-v4/googleg_disabled_color_18.png + /base/res/drawable-hdpi-v4/common_google_signin_btn_text_light_normal_background.9.png + /base/res/drawable-hdpi-v4/common_google_signin_btn_text_dark_normal_background.9.png + /base/res/drawable-hdpi-v4/common_google_signin_btn_icon_light_normal_background.9.png + /base/res/drawable-hdpi-v4/common_google_signin_btn_icon_dark_normal_background.9.png + /base/res/drawable-hdpi-v4/common_full_open_on_phone.png + /base/res/drawable-anydpi-v21/ + /base/res/drawable-anydpi-v21/ic_call_decline_low.xml + /base/res/drawable-anydpi-v21/ic_call_decline.xml + /base/res/drawable-anydpi-v21/ic_call_answer_video_low.xml + /base/res/drawable-anydpi-v21/ic_call_answer_video.xml + /base/res/drawable-anydpi-v21/ic_call_answer_low.xml + /base/res/drawable-anydpi-v21/ic_call_answer.xml + /base/res/color/ + /base/res/color/common_google_signin_btn_tint.xml + /base/res/color/common_google_signin_btn_text_light.xml + /base/res/color/common_google_signin_btn_text_dark.xml + /base/res/animator/ + /base/res/animator/fragment_open_exit.xml + /base/res/animator/fragment_open_enter.xml + /base/res/animator/fragment_fade_exit.xml + /base/res/animator/fragment_fade_enter.xml + /base/res/animator/fragment_close_exit.xml + /base/res/animator/fragment_close_enter.xml + /base/res/anim-v21/ + /base/res/anim-v21/fragment_fast_out_extra_slow_in.xml + /base/native.pb + /base/manifest/ + /base/manifest/AndroidManifest.xml + /base/lib/ + /base/lib/x86_64/ + /base/lib/x86_64/libflutter.so + /base/lib/x86_64/libdartjni.so + /base/lib/x86_64/libapp.so + /base/lib/armeabi-v7a/ + /base/lib/armeabi-v7a/libflutter.so + /base/lib/armeabi-v7a/libdartjni.so + /base/lib/armeabi-v7a/libapp.so + /base/lib/arm64-v8a/ + /base/lib/arm64-v8a/libflutter.so + /base/lib/arm64-v8a/libdartjni.so + /base/lib/arm64-v8a/libapp.so + /base/dex/ + /base/dex/classes.dex + /base/assets/ + /base/assets/flutter_assets/ + /base/assets/flutter_assets/shaders/ + /base/assets/flutter_assets/shaders/stretch_effect.frag + /base/assets/flutter_assets/shaders/ink_sparkle.frag + /base/assets/flutter_assets/packages/ + /base/assets/flutter_assets/packages/cupertino_icons/ + /base/assets/flutter_assets/packages/cupertino_icons/assets/ + /base/assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf + /base/assets/flutter_assets/fonts/ + /base/assets/flutter_assets/fonts/MaterialIcons-Regular.otf + /base/assets/flutter_assets/assets/ + /base/assets/flutter_assets/assets/preloader.png + /base/assets/flutter_assets/NativeAssetsManifest.json + /base/assets/flutter_assets/NOTICES.Z + /base/assets/flutter_assets/FontManifest.json + /base/assets/flutter_assets/AssetManifest.bin + /base/assets.pb + /BundleConfig.pb + /BUNDLE-METADATA/ + /BUNDLE-METADATA/com.android.tools/ + /BUNDLE-METADATA/com.android.tools/d8.json + /BUNDLE-METADATA/com.android.tools.build.profiles/ + /BUNDLE-METADATA/com.android.tools.build.profiles/baseline.profm + /BUNDLE-METADATA/com.android.tools.build.profiles/baseline.prof + /BUNDLE-METADATA/com.android.tools.build.libraries/ + /BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb + /BUNDLE-METADATA/com.android.tools.build.gradle/ + /BUNDLE-METADATA/com.android.tools.build.gradle/app-metadata.properties +[ ] libflutter.so.sym or libflutter.so.dbg not present when checking final appbundle for debug symbols. +[ ] "flutter appbundle" took 3,519ms. +[ +2 ms] executing: sw_vers -productName +[ +10 ms] Exit code 0 from: sw_vers -productName +[ ] macOS +[ ] executing: sw_vers -productVersion +[ +7 ms] Exit code 0 from: sw_vers -productVersion +[ ] 26.4.1 +[ ] executing: sw_vers -buildVersion +[ +6 ms] Exit code 0 from: sw_vers -buildVersion +[ ] 25E253 +[ ] executing: uname -m +[ +3 ms] Exit code 0 from: uname -m +[ ] arm64 +[ +19 ms] Release app bundle failed to strip debug symbols from native libraries. + Please run flutter doctor and ensure that the Android toolchain does not + report any issues. + + Otherwise, file an issue at https://github.com/flutter/flutter/issues. +[ ] + #0 throwToolExit (package:flutter_tools/src/base/common.dart:34:3) + #1 AndroidGradleBuilder.buildGradleApp (package:flutter_tools/src/android/gradle.dart:610:9) + + #2 AndroidGradleBuilder.buildAab (package:flutter_tools/src/android/gradle.dart:257:5) + + #3 BuildAppBundleCommand.runCommand (package:flutter_tools/src/commands/build_appbundle.dart:169:5) + + #4 FlutterCommand.run. (package:flutter_tools/src/runner/flutter_command.dart:1630:27) + + #5 AppContext.run. (package:flutter_tools/src/base/context.dart:154:19) + + #6 CommandRunner.runCommand (package:args/command_runner.dart:212:13) + + #7 FlutterCommandRunner.runCommand. (package:flutter_tools/src/runner/flutter_command_runner.dart:496:9) + + #8 AppContext.run. (package:flutter_tools/src/base/context.dart:154:19) + + #9 FlutterCommandRunner.runCommand (package:flutter_tools/src/runner/flutter_command_runner.dart:431:5) + + #10 FlutterCommandRunner.run. (package:flutter_tools/src/runner/flutter_command_runner.dart:307:33) + + #11 run.. (package:flutter_tools/runner.dart:104:11) + + #12 AppContext.run. (package:flutter_tools/src/base/context.dart:154:19) + + #13 main (package:flutter_tools/executable.dart:103:3) + + + +[ ] Running 1 shutdown hook +[ ] Shutdown hooks complete +[ +160 ms] exiting with code 1 diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e0f7b92 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 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"; + }; + 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"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..f5ea08a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..10999dd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..824e9f6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..1cd8ea9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..975c021 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..c4a6489 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..b575b60 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..824e9f6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..d56e545 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..1bfa190 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..1fe4e77 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..3641bf7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..6bc2d4f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..729b881 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..1bfa190 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..28f01b5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..b0e201a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..e55e57c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..1ab33b9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..aa48c39 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..f22d0a1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..e48e76b --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Doormile CRM + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Doormile CRM + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + NSLocationWhenInUseUsageDescription + DoorMile CRM records your GPS location when a survey is submitted so the field visit can be logged accurately. + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..0721afc --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'models/client.dart'; +import 'models/activity.dart'; +import 'models/app_stats.dart'; +import 'screens/dashboard_screen.dart'; +import 'screens/clients_screen.dart'; +import 'screens/survey_screen.dart'; +import 'screens/surveys_list_screen.dart'; +import 'screens/pricing_screen.dart'; +import 'services/crm_api_service.dart'; +import 'services/auth_service.dart'; + +class AppShell extends StatefulWidget { + final AuthUser currentUser; + final VoidCallback onLogout; + + const AppShell({ + super.key, + required this.currentUser, + required this.onLogout, + }); + + @override + State createState() => _AppShellState(); +} + +class _AppShellState extends State { + int _currentIndex = 0; + bool _backendReady = false; + + List _clients = []; + List _activities = []; + List _surveys = []; + List _pricing = []; + AppStats _stats = const AppStats( + totalClients: 0, + avgVolume: 0, + revenueOpportunity: 0, + todayEntries: 0, + pendingFollowups: 0, + ); + + final _api = CrmApiService(); + + Timer? _pollingTimer; + + @override + void initState() { + super.initState(); + _api.setToken(widget.currentUser.token); + _loadFromBackend(); + _startPolling(); + } + + void _startPolling() { + _pollingTimer = Timer.periodic(const Duration(seconds: 10), (_) { + _loadFromBackend(silent: true); + }); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } + + Future _loadFromBackend({bool silent = false}) async { + try { + final results = await Future.wait([ + _api.loadClients(), + _api.fetchSurveys(), + _api.fetchPricing(), + ]); + final clients = results[0] as List; + final surveys = results[1]; + final pricing = results[2]; + + if (!mounted) return; + setState(() { + _clients = clients; + _surveys = surveys; + _pricing = pricing; + _stats = _computeStats(clients); + _backendReady = true; + }); + } catch (_) { + if (mounted && !silent) setState(() => _backendReady = false); + } + } + + AppStats _computeStats(List clients) { + if (clients.isEmpty) { + return const AppStats( + totalClients: 0, + avgVolume: 0, + revenueOpportunity: 0, + todayEntries: 0, + pendingFollowups: 0, + ); + } + final totalVolume = clients.fold(0.0, (acc, c) => acc + c.parcelVolume); + final avg = (totalVolume / clients.length).round(); + return AppStats( + totalClients: clients.length, + avgVolume: avg, + revenueOpportunity: double.parse((totalVolume * 0.01).toStringAsFixed(2)), + todayEntries: 0, + pendingFollowups: clients.where((c) => c.dataConsent == DataConsent.basicOnly).length, + ); + } + + void _handleAddClient(Client incoming) async { + final newClient = incoming.copyWith( + recordedByName: widget.currentUser.name, + recordedByEmail: widget.currentUser.email, + recordedByRole: widget.currentUser.role, + ); + + // Optimistic UI update with a temp ID + setState(() { + _clients = [newClient, ..._clients]; + _activities = [ + Activity( + id: 'act_${DateTime.now().millisecondsSinceEpoch}', + clientName: newClient.name, + action: 'Survey submitted just now', + icon: ActivityIcon.shipping, + status: ActivityStatus.newStatus, + ), + ..._activities, + ]; + _stats = _computeStats(_clients); + }); + + // Persist to backend and swap temp ID with server-assigned ID + final saved = await _api.createClient(newClient); + if (saved != null && mounted) { + setState(() { + _clients = _clients + .map((c) => c.id == newClient.id ? saved : c) + .toList(); + }); + } + } + + void _handleUpdateNotes(String clientId, String newNotes) async { + setState(() { + _clients = _clients.map((c) { + if (c.id == clientId) { + return c.copyWith(notes: newNotes, lastUpdated: 'Updated just now'); + } + return c; + }).toList(); + + final updated = _clients.firstWhere((c) => c.id == clientId); + _activities = [ + Activity( + id: 'act_${DateTime.now().millisecondsSinceEpoch}', + clientName: updated.name, + action: 'Profile updated by Rep', + icon: ActivityIcon.edit, + ), + ..._activities, + ]; + }); + + final updated = _clients.firstWhere((c) => c.id == clientId); + await _api.updateClient(updated); + } + + void _handleCompleteClientProfile( + String clientId, + double parcelVolume, + int activeContracts, + String provider, + String notes, + ) async { + String efficiency = 'High Efficiency'; + if (provider == 'Blue Dart Express') efficiency = 'Premium Service'; + if (provider == 'Gati-KWE') efficiency = 'Cost Leader'; + if (provider == 'Delhivery') efficiency = 'Local Last Mile'; + + setState(() { + _clients = _clients.map((c) { + if (c.id != clientId) return c; + return c.copyWith( + parcelVolume: parcelVolume, + activeContracts: activeContracts, + provider: provider, + efficiency: efficiency, + notes: notes, + dataConsent: DataConsent.full, + lastUpdated: 'Just now', + surveySubmitted: true, + recordedByName: widget.currentUser.name, + recordedByEmail: widget.currentUser.email, + recordedByRole: widget.currentUser.role, + ); + }).toList(); + _stats = _computeStats(_clients); + }); + + final updated = _clients.firstWhere((c) => c.id == clientId); + await _api.updateClient(updated); + } + + void _navigateTo(int index) { + if (index == 4) { + Navigator.push(context, MaterialPageRoute( + builder: (ctx) => Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.black87), + title: const Text('Add Client', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)), + ), + body: SurveyScreen( + clients: _clients, + onAddClient: _handleAddClient, + onNavigate: (i) { + Navigator.pop(ctx); + _navigateTo(i); + }, + backendReady: _backendReady, + isActive: true, + ), + ), + )); + return; + } + setState(() => _currentIndex = index); + } + + @override + Widget build(BuildContext context) { + final screens = [ + DashboardScreen( + stats: _stats, + activities: _activities, + clients: _clients, + surveys: _surveys, + pricing: _pricing, + onNavigate: _navigateTo, + onDataChanged: _loadFromBackend, + backendReady: _backendReady, + onLogout: widget.onLogout, + isActive: _currentIndex == 0, + ), + ClientsScreen( + clients: _clients, + onUpdateClientNotes: _handleUpdateNotes, + onCompleteClientProfile: _handleCompleteClientProfile, + onNavigate: _navigateTo, + backendReady: _backendReady, + isActive: _currentIndex == 1, + ), + SurveysListScreen( + surveys: _surveys, + onDataChanged: _loadFromBackend, + isActive: _currentIndex == 2, + ), + PricingScreen( + pricing: _pricing, + backendReady: _backendReady, + onDataChanged: _loadFromBackend, + isActive: _currentIndex == 3, + ), + ]; + + return Scaffold( + body: SafeArea( + child: IndexedStack( + index: _currentIndex, + children: screens, + ), + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _currentIndex, + onDestinationSelected: _navigateTo, + height: 68, + destinations: const [ + NavigationDestination(icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Home'), + NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Clients'), + NavigationDestination(icon: Icon(Icons.storefront_outlined), selectedIcon: Icon(Icons.storefront), label: 'Surveys'), + NavigationDestination(icon: Icon(Icons.local_shipping_outlined), selectedIcon: Icon(Icons.local_shipping), label: 'Pricing'), + ], + ), + ); + } +} diff --git a/lib/data/india_cities.dart b/lib/data/india_cities.dart new file mode 100644 index 0000000..e02d294 --- /dev/null +++ b/lib/data/india_cities.dart @@ -0,0 +1,166 @@ +/// A selectable Indian city shown flight-booking style: "Chennai (MAA)". +class IndiaCity { + final String name; + final String code; // airport-style 3-letter code + final String state; + + const IndiaCity(this.name, this.code, this.state); + + String get label => '$name ($code)'; + + /// True if [query] matches the city name, code, or state. + bool matches(String query) { + final q = query.trim().toLowerCase(); + if (q.isEmpty) return true; + return name.toLowerCase().contains(q) || + code.toLowerCase().contains(q) || + state.toLowerCase().contains(q); + } +} + +/// Major Indian cities with flight-style codes. Tamil Nadu listed first +/// (primary operating region), then the rest of India. +const List indiaCities = [ + // ── Tamil Nadu ────────────────────────────────────────────────────────── + IndiaCity('Chennai', 'MAA', 'Tamil Nadu'), + IndiaCity('Coimbatore', 'CJB', 'Tamil Nadu'), + IndiaCity('Madurai', 'IXM', 'Tamil Nadu'), + IndiaCity('Tiruchirappalli', 'TRZ', 'Tamil Nadu'), + IndiaCity('Salem', 'SXV', 'Tamil Nadu'), + IndiaCity('Tuticorin', 'TCR', 'Tamil Nadu'), + IndiaCity('Tirupur', 'TUP', 'Tamil Nadu'), + IndiaCity('Erode', 'ERD', 'Tamil Nadu'), + IndiaCity('Vellore', 'VLR', 'Tamil Nadu'), + IndiaCity('Tirunelveli', 'TEN', 'Tamil Nadu'), + IndiaCity('Thanjavur', 'TNJ', 'Tamil Nadu'), + IndiaCity('Dindigul', 'DGL', 'Tamil Nadu'), + IndiaCity('Hosur', 'HSR', 'Tamil Nadu'), + IndiaCity('Nagercoil', 'NGL', 'Tamil Nadu'), + IndiaCity('Karur', 'KRR', 'Tamil Nadu'), + IndiaCity('Namakkal', 'NMK', 'Tamil Nadu'), + IndiaCity('Kanchipuram', 'KCP', 'Tamil Nadu'), + IndiaCity('Cuddalore', 'CDL', 'Tamil Nadu'), + IndiaCity('Thoothukudi', 'TOO', 'Tamil Nadu'), + + // ── Karnataka ─────────────────────────────────────────────────────────── + IndiaCity('Bengaluru', 'BLR', 'Karnataka'), + IndiaCity('Mysuru', 'MYQ', 'Karnataka'), + IndiaCity('Mangaluru', 'IXE', 'Karnataka'), + IndiaCity('Hubli', 'HBX', 'Karnataka'), + IndiaCity('Belagavi', 'IXG', 'Karnataka'), + IndiaCity('Kalaburagi', 'GBI', 'Karnataka'), + IndiaCity('Davangere', 'DVG', 'Karnataka'), + IndiaCity('Ballari', 'BEP', 'Karnataka'), + + // ── Kerala ────────────────────────────────────────────────────────────── + IndiaCity('Kochi', 'COK', 'Kerala'), + IndiaCity('Thiruvananthapuram', 'TRV', 'Kerala'), + IndiaCity('Kozhikode', 'CCJ', 'Kerala'), + IndiaCity('Kannur', 'CNN', 'Kerala'), + IndiaCity('Thrissur', 'TCR2', 'Kerala'), + IndiaCity('Kollam', 'QLN', 'Kerala'), + + // ── Telangana & Andhra Pradesh ────────────────────────────────────────── + IndiaCity('Hyderabad', 'HYD', 'Telangana'), + IndiaCity('Warangal', 'WGC', 'Telangana'), + IndiaCity('Visakhapatnam', 'VTZ', 'Andhra Pradesh'), + IndiaCity('Vijayawada', 'VGA', 'Andhra Pradesh'), + IndiaCity('Tirupati', 'TIR', 'Andhra Pradesh'), + IndiaCity('Guntur', 'GNT', 'Andhra Pradesh'), + IndiaCity('Rajahmundry', 'RJA', 'Andhra Pradesh'), + IndiaCity('Nellore', 'NLR', 'Andhra Pradesh'), + + // ── Maharashtra ───────────────────────────────────────────────────────── + IndiaCity('Mumbai', 'BOM', 'Maharashtra'), + IndiaCity('Pune', 'PNQ', 'Maharashtra'), + IndiaCity('Nagpur', 'NAG', 'Maharashtra'), + IndiaCity('Nashik', 'ISK', 'Maharashtra'), + IndiaCity('Aurangabad', 'IXU', 'Maharashtra'), + IndiaCity('Kolhapur', 'KLH', 'Maharashtra'), + IndiaCity('Solapur', 'SSE', 'Maharashtra'), + + // ── Delhi NCR & North ─────────────────────────────────────────────────── + IndiaCity('New Delhi', 'DEL', 'Delhi'), + IndiaCity('Gurugram', 'GGN', 'Haryana'), + IndiaCity('Noida', 'NOI', 'Uttar Pradesh'), + IndiaCity('Faridabad', 'FBD', 'Haryana'), + IndiaCity('Chandigarh', 'IXC', 'Chandigarh'), + IndiaCity('Amritsar', 'ATQ', 'Punjab'), + IndiaCity('Ludhiana', 'LUH', 'Punjab'), + IndiaCity('Jalandhar', 'JLR2', 'Punjab'), + + // ── Gujarat ───────────────────────────────────────────────────────────── + IndiaCity('Ahmedabad', 'AMD', 'Gujarat'), + IndiaCity('Surat', 'STV', 'Gujarat'), + IndiaCity('Vadodara', 'BDQ', 'Gujarat'), + IndiaCity('Rajkot', 'RAJ', 'Gujarat'), + IndiaCity('Bhavnagar', 'BHU', 'Gujarat'), + IndiaCity('Jamnagar', 'JGA', 'Gujarat'), + + // ── Rajasthan ─────────────────────────────────────────────────────────── + IndiaCity('Jaipur', 'JAI', 'Rajasthan'), + IndiaCity('Jodhpur', 'JDH', 'Rajasthan'), + IndiaCity('Udaipur', 'UDR', 'Rajasthan'), + IndiaCity('Kota', 'KTU', 'Rajasthan'), + IndiaCity('Bikaner', 'BKB', 'Rajasthan'), + + // ── Uttar Pradesh & Central ───────────────────────────────────────────── + IndiaCity('Lucknow', 'LKO', 'Uttar Pradesh'), + IndiaCity('Kanpur', 'KNU', 'Uttar Pradesh'), + IndiaCity('Varanasi', 'VNS', 'Uttar Pradesh'), + IndiaCity('Agra', 'AGR', 'Uttar Pradesh'), + IndiaCity('Prayagraj', 'IXD', 'Uttar Pradesh'), + IndiaCity('Gorakhpur', 'GOP', 'Uttar Pradesh'), + IndiaCity('Bhopal', 'BHO', 'Madhya Pradesh'), + IndiaCity('Indore', 'IDR', 'Madhya Pradesh'), + IndiaCity('Gwalior', 'GWL', 'Madhya Pradesh'), + IndiaCity('Jabalpur', 'JLR', 'Madhya Pradesh'), + IndiaCity('Raipur', 'RPR', 'Chhattisgarh'), + + // ── East & North-East ─────────────────────────────────────────────────── + IndiaCity('Kolkata', 'CCU', 'West Bengal'), + IndiaCity('Siliguri', 'IXB', 'West Bengal'), + IndiaCity('Durgapur', 'RDP', 'West Bengal'), + IndiaCity('Patna', 'PAT', 'Bihar'), + IndiaCity('Gaya', 'GAY', 'Bihar'), + IndiaCity('Ranchi', 'IXR', 'Jharkhand'), + IndiaCity('Jamshedpur', 'JSD', 'Jharkhand'), + IndiaCity('Bhubaneswar', 'BBI', 'Odisha'), + IndiaCity('Cuttack', 'CTC', 'Odisha'), + IndiaCity('Guwahati', 'GAU', 'Assam'), + IndiaCity('Dibrugarh', 'DIB', 'Assam'), + IndiaCity('Agartala', 'IXA', 'Tripura'), + IndiaCity('Imphal', 'IMF', 'Manipur'), + IndiaCity('Shillong', 'SHL', 'Meghalaya'), + IndiaCity('Aizawl', 'AJL', 'Mizoram'), + IndiaCity('Dimapur', 'DMU', 'Nagaland'), + + // ── Hills, UTs & others ───────────────────────────────────────────────── + IndiaCity('Dehradun', 'DED', 'Uttarakhand'), + IndiaCity('Haridwar', 'HDW', 'Uttarakhand'), + IndiaCity('Shimla', 'SLV', 'Himachal Pradesh'), + IndiaCity('Srinagar', 'SXR', 'Jammu & Kashmir'), + IndiaCity('Jammu', 'IXJ', 'Jammu & Kashmir'), + IndiaCity('Leh', 'IXL', 'Ladakh'), + IndiaCity('Panaji', 'GOI', 'Goa'), + IndiaCity('Puducherry', 'PNY', 'Puducherry'), + IndiaCity('Port Blair', 'IXZ', 'Andaman & Nicobar Islands'), +]; + +/// Finds a city by (loose) name match — used to map a GPS reverse-geocode +/// result to a known city. Returns null if nothing reasonable matches. +IndiaCity? findCityByName(String? name) { + if (name == null || name.trim().isEmpty) return null; + final n = name.trim().toLowerCase(); + for (final c in indiaCities) { + if (c.name.toLowerCase() == n) return c; + } + // Loose contains match (e.g. "Coimbatore District" → "Coimbatore") + for (final c in indiaCities) { + if (n.contains(c.name.toLowerCase()) || + c.name.toLowerCase().contains(n)) { + return c; + } + } + return null; +} diff --git a/lib/data/india_locations.dart b/lib/data/india_locations.dart new file mode 100644 index 0000000..f819557 --- /dev/null +++ b/lib/data/india_locations.dart @@ -0,0 +1,221 @@ +/// All Indian states + Union Territories, alphabetically sorted. +const List indianStates = [ + 'Andaman & Nicobar Islands', + 'Andhra Pradesh', + 'Arunachal Pradesh', + 'Assam', + 'Bihar', + 'Chandigarh', + 'Chhattisgarh', + 'Dadra & Nagar Haveli', + 'Daman & Diu', + 'Delhi', + 'Goa', + 'Gujarat', + 'Haryana', + 'Himachal Pradesh', + 'Jammu & Kashmir', + 'Jharkhand', + 'Karnataka', + 'Kerala', + 'Ladakh', + 'Lakshadweep', + 'Madhya Pradesh', + 'Maharashtra', + 'Manipur', + 'Meghalaya', + 'Mizoram', + 'Nagaland', + 'Odisha', + 'Puducherry', + 'Punjab', + 'Rajasthan', + 'Sikkim', + 'Tamil Nadu', + 'Telangana', + 'Tripura', + 'Uttar Pradesh', + 'Uttarakhand', + 'West Bengal', +]; + +/// Major cities per state. Falls back to an empty list for states not listed. +const Map> stateCities = { + 'Tamil Nadu': [ + 'Chennai', 'Coimbatore', 'Madurai', 'Salem', 'Tirupur', 'Erode', + 'Tiruchirappalli', 'Tirunelveli', 'Vellore', 'Dindigul', 'Thanjavur', + 'Kanchipuram', 'Namakkal', 'Karur', 'Dharmapuri', 'Krishnagiri', + 'Cuddalore', 'Villupuram', 'Nagapattinam', 'Thoothukudi', 'Virudhunagar', + 'Sivaganga', 'Ramanathapuram', 'Theni', 'Perambalur', 'Puducherry', + ], + 'Karnataka': [ + 'Bengaluru', 'Mysuru', 'Mangaluru', 'Hubli', 'Belagavi', + 'Davangere', 'Shivamogga', 'Tumakuru', 'Kalaburagi', 'Udupi', + 'Ballari', 'Bidar', 'Vijayapura', 'Raichur', 'Dharwad', 'Hassan', + ], + 'Kerala': [ + 'Kochi', 'Thiruvananthapuram', 'Kozhikode', 'Thrissur', 'Kannur', + 'Kollam', 'Palakkad', 'Malappuram', 'Alappuzha', 'Kottayam', + 'Idukki', 'Wayanad', 'Kasaragod', 'Pathanamthitta', + ], + 'Andhra Pradesh': [ + 'Vijayawada', 'Visakhapatnam', 'Tirupati', 'Guntur', 'Nellore', + 'Kakinada', 'Rajahmundry', 'Kadapa', 'Kurnool', 'Ongole', + 'Anantapur', 'Vizianagaram', 'Eluru', + ], + 'Telangana': [ + 'Hyderabad', 'Warangal', 'Nizamabad', 'Karimnagar', 'Khammam', + 'Mahbubnagar', 'Nalgonda', 'Adilabad', 'Secunderabad', 'Siddipet', + ], + 'Maharashtra': [ + 'Mumbai', 'Pune', 'Nagpur', 'Nashik', 'Aurangabad', 'Thane', + 'Solapur', 'Amravati', 'Kolhapur', 'Nanded', 'Sangli', 'Jalgaon', + 'Akola', 'Latur', 'Chandrapur', + ], + 'Delhi': [ + 'New Delhi', 'North Delhi', 'South Delhi', 'East Delhi', 'West Delhi', + 'Noida', 'Gurgaon', 'Faridabad', 'Ghaziabad', 'Dwarka', + ], + 'Gujarat': [ + 'Ahmedabad', 'Surat', 'Vadodara', 'Rajkot', 'Gandhinagar', + 'Bhavnagar', 'Jamnagar', 'Junagadh', 'Anand', 'Bharuch', + 'Morbi', 'Nadiad', 'Surendranagar', + ], + 'Rajasthan': [ + 'Jaipur', 'Jodhpur', 'Udaipur', 'Kota', 'Ajmer', + 'Bikaner', 'Alwar', 'Bharatpur', 'Sikar', 'Sri Ganganagar', + 'Pali', 'Tonk', 'Barmer', + ], + 'West Bengal': [ + 'Kolkata', 'Howrah', 'Durgapur', 'Asansol', 'Siliguri', + 'Bardhaman', 'Malda', 'Kharagpur', 'Haldia', 'Jalpaiguri', + ], + 'Uttar Pradesh': [ + 'Lucknow', 'Kanpur', 'Varanasi', 'Agra', 'Meerut', + 'Prayagraj', 'Bareilly', 'Moradabad', 'Gorakhpur', 'Aligarh', + 'Noida', 'Ghaziabad', 'Firozabad', 'Saharanpur', 'Mathura', + ], + 'Madhya Pradesh': [ + 'Bhopal', 'Indore', 'Gwalior', 'Jabalpur', 'Ujjain', + 'Sagar', 'Dewas', 'Satna', 'Ratlam', 'Rewa', 'Singrauli', + ], + 'Punjab': [ + 'Ludhiana', 'Amritsar', 'Jalandhar', 'Patiala', 'Bathinda', + 'Mohali', 'Hoshiarpur', 'Gurdaspur', 'Pathankot', 'Firozpur', + ], + 'Haryana': [ + 'Gurugram', 'Faridabad', 'Hisar', 'Panipat', 'Ambala', + 'Karnal', 'Rohtak', 'Bhiwani', 'Sirsa', 'Sonipat', + ], + 'Bihar': [ + 'Patna', 'Gaya', 'Bhagalpur', 'Muzaffarpur', 'Purnia', + 'Darbhanga', 'Arrah', 'Begusarai', 'Munger', 'Chhapra', + ], + 'Odisha': [ + 'Bhubaneswar', 'Cuttack', 'Rourkela', 'Brahmapur', 'Puri', + 'Sambalpur', 'Balasore', 'Baripada', 'Jharsuguda', + ], + 'Assam': [ + 'Guwahati', 'Silchar', 'Dibrugarh', 'Jorhat', 'Nagaon', + 'Tinsukia', 'Tezpur', 'Bongaigaon', 'Karimganj', + ], + 'Jharkhand': [ + 'Ranchi', 'Jamshedpur', 'Dhanbad', 'Bokaro', 'Deoghar', + 'Hazaribagh', 'Giridih', 'Dumka', 'Phusro', + ], + 'Chhattisgarh': [ + 'Raipur', 'Bhilai', 'Bilaspur', 'Korba', 'Rajnandgaon', + 'Durg', 'Jagdalpur', 'Ambikapur', 'Raigarh', + ], + 'Uttarakhand': [ + 'Dehradun', 'Haridwar', 'Rishikesh', 'Roorkee', 'Haldwani', + 'Nainital', 'Mussoorie', 'Kashipur', 'Rudrapur', + ], + 'Himachal Pradesh': [ + 'Shimla', 'Dharamshala', 'Solan', 'Mandi', 'Kullu', + 'Manali', 'Baddi', 'Bilaspur', 'Kangra', + ], + 'Goa': [ + 'Panaji', 'Margao', 'Vasco da Gama', 'Mapusa', 'Ponda', 'Calangute', + ], + 'Puducherry': ['Puducherry', 'Karaikal', 'Mahe', 'Yanam'], + 'Jammu & Kashmir': [ + 'Srinagar', 'Jammu', 'Anantnag', 'Baramulla', 'Sopore', 'Udhampur', + ], + 'Chandigarh': ['Chandigarh'], + 'Ladakh': ['Leh', 'Kargil'], + 'Andaman & Nicobar Islands': ['Port Blair'], + 'Manipur': ['Imphal', 'Thoubal', 'Bishnupur', 'Churachandpur'], + 'Meghalaya': ['Shillong', 'Tura', 'Jowai'], + 'Mizoram': ['Aizawl', 'Lunglei', 'Champhai'], + 'Nagaland': ['Kohima', 'Dimapur', 'Mokokchung'], + 'Arunachal Pradesh': ['Itanagar', 'Naharlagun', 'Pasighat', 'Tawang'], + 'Sikkim': ['Gangtok', 'Namchi', 'Gyalshing'], + 'Tripura': ['Agartala', 'Dharmanagar', 'Udaipur', 'Kailasahar'], +}; + +const Map> categorisedProviders = { + 'National & Pan-India Courier Services (Parcels & Express)': [ + 'Blue Dart', + 'Delhivery', + 'DTDC', + 'India Post / Speed Post', + 'The Professional Couriers', + 'XpressBees', + 'Ecom Express', + 'Shadowfax', + ], + 'National Heavyweight Cargo & B2B Surface Freight': [ + 'Safexpress', + 'VRL Logistics', + 'TCI (Transport Corporation of India)', + 'Om Logistics', + 'Best Roadways', + ], + 'Southern India Dual-Purpose & Regional Networks (Tamil Nadu, Telangana, AP, Karnataka)': [ + 'MSS (Mettur Super Services / Mettur Transports)', + 'ABT Travels & Logistics', + 'Navata Road Transport', + 'KRS (Kerala Roadways)', + 'Parveen Travels / Parveen Express', + 'SRM Transports', + 'KPN Travels & KPN Speed Parcel', + 'SRS Travels', + ], + 'Hyper-Local & District Operators in Tamil Nadu': [ + 'Hindusthan Travels', + 'City Travels', + 'Essaar Travels', + 'No. 1 Air Travels', + 'A1 Travels', + 'Krish Travels', + 'Hebron Transports', + 'Horma Travels', + 'Vivegam Travels', + 'PSS Transport', + 'SRT (Renugambal Travels)', + 'Thamarai Bus Transports', + 'Rathimeena Travels', + 'Ganesh Travels', + 'John Kennedy Bus Service', + 'Arun Travel', + 'Saaji Meera Roadways', + 'ARC Parcel Service', + 'Chakra Travels & Parcel Service', + 'MVA Parcel And Bus Service', + ], + 'Northern & Central India Regional Operators (Delhi NCR, Rajasthan, UP, MP, Punjab, Haryana)': [ + 'Shrinath Travels & Cargo', + 'Hans Travels', + 'Zingbus', + 'Kalpana Travels / City Land Travels', + 'Trackon Couriers', + 'North India Transways', + 'RSRTC Cargo', + 'UPSRTC Cargo', + ], +}; + +final List allProvidersFlat = + categorisedProviders.values.expand((list) => list).toList(); + diff --git a/lib/data/initial_data.dart b/lib/data/initial_data.dart new file mode 100644 index 0000000..da3d7fd --- /dev/null +++ b/lib/data/initial_data.dart @@ -0,0 +1,46 @@ +import '../models/client.dart'; +import '../models/activity.dart'; +import '../models/app_stats.dart'; + +const AppStats initialStats = AppStats( + totalClients: 1, + avgVolume: 200, + revenueOpportunity: 0.1, + todayEntries: 1, + pendingFollowups: 0, +); + +const List initialClients = [ + Client( + id: 'suriya_001', + name: 'Suriya', + city: 'Coimbatore', + businessType: BusinessType.ecommerce, + parcelVolume: 200, + activeContracts: 5, + provider: 'DTDC Express', + efficiency: 'High Efficiency', + status: ClientStatus.newClient, + lastUpdated: 'Just now', + surveySubmitted: true, + notes: '', + dataConsent: DataConsent.full, + phone: '', + frequency: 'Weekly', + businessState: 'Tamil Nadu', + transitType: 'within_state', + transitFrom: 'Coimbatore', + transitTo: 'Chennai', + neighbourhood: 'RS Puram', + ), +]; + +const List initialActivities = [ + Activity( + id: 'a1', + clientName: 'Suriya', + action: 'Profile created — staging entry', + icon: ActivityIcon.shipping, + status: ActivityStatus.newStatus, + ), +]; diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..2e8b85a --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'app.dart'; +import 'screens/login_screen.dart'; +import 'screens/update_screen.dart'; +import 'services/auth_service.dart'; +import 'package:in_app_update/in_app_update.dart'; +import 'dart:io'; +import 'theme/app_theme.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + ); + + AuthUser? initialUser; + try { + initialUser = await AuthService().getSavedUser(); + } catch (_) {} + + runApp(DoorMileApp(initialUser: initialUser)); +} + +class DoorMileApp extends StatelessWidget { + final AuthUser? initialUser; + + const DoorMileApp({super.key, this.initialUser}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Doormile CRM', + debugShowCheckedModeBanner: false, + theme: buildAppTheme(), + home: AuthGate(initialUser: initialUser), + builder: (context, child) { + final mediaQuery = MediaQuery.of(context); + return Container( + color: AppColors.scaffold, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: MediaQuery( + data: mediaQuery.copyWith( + textScaler: mediaQuery.textScaler.clamp( + minScaleFactor: 0.85, + maxScaleFactor: 1.15, + ), + ), + child: child!, + ), + ), + ), + ); + }, + ); + } +} + +class AuthGate extends StatefulWidget { + final AuthUser? initialUser; + + const AuthGate({super.key, this.initialUser}); + + @override + State createState() => _AuthGateState(); +} + +class _AuthGateState extends State { + AuthUser? _user; + bool _updateRequired = false; + + @override + void initState() { + super.initState(); + _user = widget.initialUser; + _checkForUpdates(); + } + + Future _checkForUpdates() async { + try { + if (Platform.isAndroid) { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability == UpdateAvailability.updateAvailable) { + setState(() => _updateRequired = true); + await InAppUpdate.performImmediateUpdate(); + } + } + } catch (e) { + // Ignored: Not a supported Android device or Play Store not accessible + } + } + + Future _login(AuthUser user) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_user_data', jsonEncode(user.toJson())); + await prefs.setString('user_name', user.name); + } catch (_) {} + setState(() => _user = user); + } + + Future _logout() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('auth_user_data'); + await prefs.remove('user_name'); + } catch (_) {} + setState(() => _user = null); + } + + @override + Widget build(BuildContext context) { + if (_updateRequired) { + return const UpdateRequiredScreen(); + } + if (_user == null) { + return LoginScreen( + onLoggedIn: _login, + ); + } + return AppShell(currentUser: _user!, onLogout: _logout); + } +} diff --git a/lib/models/activity.dart b/lib/models/activity.dart new file mode 100644 index 0000000..ce6b119 --- /dev/null +++ b/lib/models/activity.dart @@ -0,0 +1,19 @@ +enum ActivityIcon { shipping, call, edit } + +enum ActivityStatus { newStatus, pending, completed } + +class Activity { + final String id; + final String clientName; + final String action; + final ActivityIcon icon; + final ActivityStatus? status; + + const Activity({ + required this.id, + required this.clientName, + required this.action, + required this.icon, + this.status, + }); +} diff --git a/lib/models/app_stats.dart b/lib/models/app_stats.dart new file mode 100644 index 0000000..dd4c620 --- /dev/null +++ b/lib/models/app_stats.dart @@ -0,0 +1,31 @@ +class AppStats { + final int totalClients; + final int avgVolume; + final double revenueOpportunity; + final int todayEntries; + final int pendingFollowups; + + const AppStats({ + required this.totalClients, + required this.avgVolume, + required this.revenueOpportunity, + required this.todayEntries, + required this.pendingFollowups, + }); + + AppStats copyWith({ + int? totalClients, + int? avgVolume, + double? revenueOpportunity, + int? todayEntries, + int? pendingFollowups, + }) { + return AppStats( + totalClients: totalClients ?? this.totalClients, + avgVolume: avgVolume ?? this.avgVolume, + revenueOpportunity: revenueOpportunity ?? this.revenueOpportunity, + todayEntries: todayEntries ?? this.todayEntries, + pendingFollowups: pendingFollowups ?? this.pendingFollowups, + ); + } +} diff --git a/lib/models/client.dart b/lib/models/client.dart new file mode 100644 index 0000000..c5d6b87 --- /dev/null +++ b/lib/models/client.dart @@ -0,0 +1,173 @@ +enum DataConsent { basicOnly, full } + +enum BusinessType { ecommerce, manufacturing, wholesale, retail } + +enum ClientStatus { newClient, pending, completed, updated } + +extension BusinessTypeLabel on BusinessType { + String get label { + switch (this) { + case BusinessType.ecommerce: + return 'E-commerce'; + case BusinessType.manufacturing: + return 'Manufacturing'; + case BusinessType.wholesale: + return 'Wholesale'; + case BusinessType.retail: + return 'Retail'; + } + } +} + +extension ClientStatusLabel on ClientStatus { + String get label { + switch (this) { + case ClientStatus.newClient: + return 'NEW'; + case ClientStatus.pending: + return 'PENDING'; + case ClientStatus.completed: + return 'COMPLETED'; + case ClientStatus.updated: + return 'UPDATED'; + } + } +} + +class Client { + final String id; + final String name; + final String city; // free-text, e.g. "New York", "Austin" + final BusinessType businessType; + final double parcelVolume; + final int activeContracts; + final String provider; + final String efficiency; + final ClientStatus status; + final String lastUpdated; + final bool surveySubmitted; + final String notes; + final DataConsent dataConsent; + + // Extended basic-info fields + final String phone; // contact number + final String frequency; // shipment frequency: Daily / Weekly / etc. + final String businessState; // Indian state of the business + final String transitType; // 'within_state' | 'state_to_state' | '' + final String transitFrom; // city (within_state) or state name (state_to_state) + final String transitTo; // city (within_state) or state name (state_to_state) + final String neighbourhood; // local area / neighbourhood + final String logisticsSegment; // 'First Mile' | 'Middle Mile' | 'Last Mile' | '' + final String pincode; // client postal/PIN code — optional manual entry + + // Device GPS at the moment of survey submission — stored for field audit, not displayed in app UI. + final double surveyLat; + final double surveyLng; + final String surveyAddress; // reverse-geocoded full address + final String surveyZone; // suburb / neighbourhood from geocoder + final String surveyPincode; // postal code from GPS reverse-geocode + + // Field-agent attribution — who collected this record (from login session). + final String recordedByName; // e.g. "Kamesh" + final String recordedByEmail; // login email of the agent + final String recordedByRole; // 'admin' | 'rep' | 'support' + + const Client({ + required this.id, + required this.name, + required this.city, + required this.businessType, + required this.parcelVolume, + required this.activeContracts, + required this.provider, + required this.efficiency, + required this.status, + required this.lastUpdated, + required this.surveySubmitted, + this.notes = '', + this.dataConsent = DataConsent.full, + this.phone = '', + this.frequency = '', + this.businessState = '', + this.transitType = '', + this.transitFrom = '', + this.transitTo = '', + this.neighbourhood = '', + this.logisticsSegment = '', + this.pincode = '', + this.surveyLat = 0.0, + this.surveyLng = 0.0, + this.surveyAddress = '', + this.surveyZone = '', + this.surveyPincode = '', + this.recordedByName = '', + this.recordedByEmail = '', + this.recordedByRole = '', + }); + + Client copyWith({ + String? id, + String? name, + String? city, + BusinessType? businessType, + double? parcelVolume, + int? activeContracts, + String? provider, + String? efficiency, + ClientStatus? status, + String? lastUpdated, + bool? surveySubmitted, + String? notes, + DataConsent? dataConsent, + String? phone, + String? frequency, + String? businessState, + String? transitType, + String? transitFrom, + String? transitTo, + String? neighbourhood, + String? logisticsSegment, + String? pincode, + double? surveyLat, + double? surveyLng, + String? surveyAddress, + String? surveyZone, + String? surveyPincode, + String? recordedByName, + String? recordedByEmail, + String? recordedByRole, + }) { + return Client( + id: id ?? this.id, + name: name ?? this.name, + city: city ?? this.city, + businessType: businessType ?? this.businessType, + parcelVolume: parcelVolume ?? this.parcelVolume, + activeContracts: activeContracts ?? this.activeContracts, + provider: provider ?? this.provider, + efficiency: efficiency ?? this.efficiency, + status: status ?? this.status, + lastUpdated: lastUpdated ?? this.lastUpdated, + surveySubmitted: surveySubmitted ?? this.surveySubmitted, + notes: notes ?? this.notes, + dataConsent: dataConsent ?? this.dataConsent, + phone: phone ?? this.phone, + frequency: frequency ?? this.frequency, + businessState: businessState ?? this.businessState, + transitType: transitType ?? this.transitType, + transitFrom: transitFrom ?? this.transitFrom, + transitTo: transitTo ?? this.transitTo, + neighbourhood: neighbourhood ?? this.neighbourhood, + logisticsSegment: logisticsSegment ?? this.logisticsSegment, + pincode: pincode ?? this.pincode, + surveyLat: surveyLat ?? this.surveyLat, + surveyLng: surveyLng ?? this.surveyLng, + surveyAddress: surveyAddress ?? this.surveyAddress, + surveyZone: surveyZone ?? this.surveyZone, + surveyPincode: surveyPincode ?? this.surveyPincode, + recordedByName: recordedByName ?? this.recordedByName, + recordedByEmail: recordedByEmail ?? this.recordedByEmail, + recordedByRole: recordedByRole ?? this.recordedByRole, + ); + } +} diff --git a/lib/screens/clients_screen.dart b/lib/screens/clients_screen.dart new file mode 100644 index 0000000..8d6fbe2 --- /dev/null +++ b/lib/screens/clients_screen.dart @@ -0,0 +1,1483 @@ +import 'package:flutter/material.dart'; +import '../models/client.dart'; +import '../theme/app_theme.dart'; +import '../utils/snackbar_utils.dart'; +import 'survey_screen.dart' show ConsentBadge, ProviderPickerSheet; + +class ClientsScreen extends StatefulWidget { + final List clients; + final void Function(String clientId, String notes) onUpdateClientNotes; + final void Function( + String clientId, + double parcelVolume, + int activeContracts, + String provider, + String notes, + ) onCompleteClientProfile; + final void Function(int) onNavigate; + final bool backendReady; + final bool isActive; + + const ClientsScreen({ + super.key, + required this.clients, + required this.onUpdateClientNotes, + required this.onCompleteClientProfile, + required this.onNavigate, + required this.backendReady, + this.isActive = false, + }); + + @override + State createState() => _ClientsScreenState(); +} + +class _ClientsScreenState extends State { + final _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + String _search = ''; + String? _selectedCity; + BusinessType? _selectedType; + + @override + void didUpdateWidget(covariant ClientsScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isActive && !oldWidget.isActive) { + if (_scrollController.hasClients) { + _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + } + } + + @override + void dispose() { + _searchController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + /// Unique city strings from the client list for dynamic filter chips. + List get _uniqueCities { + final seen = {}; + return widget.clients + .map((c) => c.city) + .where((c) => c.isNotEmpty && seen.add(c)) + .toList(); + } + + List get _filtered { + return widget.clients.where((c) { + final q = _search.toLowerCase(); + final matchSearch = _search.isEmpty || + c.name.toLowerCase().contains(q) || + c.city.toLowerCase().contains(q) || + c.provider.toLowerCase().contains(q) || + c.neighbourhood.toLowerCase().contains(q); + final matchCity = + _selectedCity == null || c.city == _selectedCity; + final matchType = + _selectedType == null || c.businessType == _selectedType; + return matchSearch && matchCity && matchType; + }).toList(); + } + + void _openClient(Client client) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _ClientDetailSheet( + client: client, + onSaveNotes: (notes) => + widget.onUpdateClientNotes(client.id, notes), + onCompleteProfile: (vol, contracts, provider, notes) { + widget.onCompleteClientProfile( + client.id, vol, contracts, provider, notes); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + final filtered = _filtered; + final cities = _uniqueCities; + + return SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.fromLTRB(16, 20, 16, 100), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WideTopBanner( + backendReady: widget.backendReady, + subtitle: 'CLIENT DATABASE', + ), + const SizedBox(height: 16), + Text('Search, audit, and coordinate logistics survey reports.', + style: + TextStyle(fontSize: 13, color: AppColors.textSecondary)), + const SizedBox(height: 16), + + // Search + TextField( + controller: _searchController, + onChanged: (v) => setState(() => _search = v), + decoration: InputDecoration( + hintText: 'Search company, city, area or provider…', + hintStyle: + TextStyle(color: AppColors.muted, fontSize: 13), + prefixIcon: + Icon(Icons.search, color: AppColors.muted, size: 20), + ), + ), + const SizedBox(height: 14), + + // City filter — built dynamically from actual client data + if (cities.isNotEmpty) ...[ + Text('FILTER BY CITY', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + color: AppColors.textSecondary)), + const SizedBox(height: 6), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _FilterChip( + label: 'All', + selected: _selectedCity == null, + activeColor: AppColors.primary, + onTap: () => setState(() => _selectedCity = null), + ), + ...cities.map((city) => _FilterChip( + label: city, + selected: _selectedCity == city, + activeColor: AppColors.primary, + onTap: () => + setState(() => _selectedCity = city), + )), + ], + ), + ), + const SizedBox(height: 12), + ], + + // Business type filter + Text('FILTER BY CLASSIFICATION', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + color: AppColors.textSecondary)), + const SizedBox(height: 6), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _FilterChip( + label: 'All', + selected: _selectedType == null, + activeColor: AppColors.green, + onTap: () => setState(() => _selectedType = null), + ), + ...BusinessType.values.map( + (t) => _FilterChip( + label: t.label, + selected: _selectedType == t, + activeColor: AppColors.green, + onTap: () => setState(() => _selectedType = t), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Results + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('RESULTS (${filtered.length})', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + Text('CURRENT STACK', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + ], + ), + const SizedBox(height: 10), + + if (filtered.isEmpty) + _EmptyState(onNavigate: widget.onNavigate) + else + Column( + children: filtered + .map((c) => _ClientCard( + client: c, onTap: () => _openClient(c))) + .toList(), + ), + ], + ), + ); + } +} + +// ── Filter chip ─────────────────────────────────────────────────────────────── + +class _FilterChip extends StatelessWidget { + final String label; + final bool selected; + final Color activeColor; + final VoidCallback onTap; + + const _FilterChip({ + required this.label, + required this.selected, + required this.activeColor, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 6), + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + color: selected ? activeColor : const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(20), + ), + child: Text(label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: selected + ? Colors.white + : AppColors.textSecondary)), + ), + ), + ); + } +} + +// ── Client card ─────────────────────────────────────────────────────────────── + +class _ClientCard extends StatelessWidget { + final Client client; + final VoidCallback onTap; + + const _ClientCard({required this.client, required this.onTap}); + + @override + Widget build(BuildContext context) { + Color typeBg; + Color typeFg; + switch (client.businessType) { + case BusinessType.ecommerce: + typeBg = const Color(0xFFFFDAD7); + typeFg = const Color(0xFF930013); + break; + case BusinessType.manufacturing: + typeBg = const Color(0xFFD8F9EE); + typeFg = const Color(0xFF00504A); + break; + case BusinessType.wholesale: + typeBg = const Color(0xFFD8E3FB); + typeFg = const Color(0xFF3C475A); + break; + case BusinessType.retail: + typeBg = const Color(0xFFEFF4FF); + typeFg = const Color(0xFF545F73); + break; + } + + final isBasic = client.dataConsent == DataConsent.basicOnly; + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isBasic + ? const Color(0xFFFFB800).withValues(alpha: 0.35) + : AppColors.border.withValues(alpha: 0.25), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name + type badge + consent badge + Row(children: [ + Flexible( + child: Text(client.name, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: typeBg, + borderRadius: BorderRadius.circular(4)), + child: Text(client.businessType.label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: typeFg)), + ), + if (isBasic) ...[ + const SizedBox(width: 5), + const ConsentBadge(small: true), + ], + ]), + const SizedBox(height: 6), + // City + neighbourhood + route/provider row + Wrap(spacing: 10, runSpacing: 4, children: [ + if (client.city.isNotEmpty) + _CardMeta( + icon: Icons.location_on, + color: AppColors.primary, + text: client.neighbourhood.isNotEmpty + ? '${client.city} Ā· ${client.neighbourhood}' + : client.city), + if (client.frequency.isNotEmpty) + _CardMeta( + icon: Icons.repeat, + color: AppColors.textSecondary, + text: client.frequency), + if (!isBasic) + _CardMeta( + icon: Icons.local_shipping, + color: AppColors.green, + text: client.provider), + if (isBasic) + _CardMeta( + icon: Icons.lock_outline, + color: const Color(0xFFE87C00), + text: 'Not disclosed'), + ]), + ], + ), + ), + const SizedBox(width: 10), + if (isBasic) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF8ED), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: const Color(0xFFFFB800).withValues(alpha: 0.4)), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add_circle_outline, + size: 16, color: Color(0xFFE87C00)), + SizedBox(height: 2), + Text('Complete', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: Color(0xFFE87C00))), + ], + ), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + RichText( + text: TextSpan(children: [ + TextSpan( + text: '${client.parcelVolume.round()}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue, + fontFamily: 'monospace')), + TextSpan( + text: ' /wk', + style: TextStyle( + fontSize: 10, + color: AppColors.textSecondary)), + ]), + ), + Text('${client.activeContracts} contracts', + style: TextStyle( + fontSize: 10, + color: AppColors.textSecondary)), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _CardMeta extends StatelessWidget { + final IconData icon; + final Color color; + final String text; + const _CardMeta( + {required this.icon, required this.color, required this.text}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: color), + const SizedBox(width: 2), + Text(text, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ], + ); + } +} + +// ── Empty state ─────────────────────────────────────────────────────────────── + +class _EmptyState extends StatelessWidget { + final void Function(int) onNavigate; + const _EmptyState({required this.onNavigate}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(40), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border.withValues(alpha: 0.15)), + ), + child: Column(children: [ + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + color: Color(0xFFEFF4FF), shape: BoxShape.circle), + child: + Icon(Icons.filter_list, size: 24, color: AppColors.primary), + ), + const SizedBox(height: 12), + const Text('No matching clients found', + style: TextStyle( + fontWeight: FontWeight.w700, color: AppColors.darkBlue)), + const SizedBox(height: 4), + Text('Try adjusting search or filters.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, color: AppColors.textSecondary)), + const SizedBox(height: 14), + ElevatedButton( + onPressed: () => onNavigate(2), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + child: const Text('Add New Client', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w700)), + ), + ]), + ); + } +} + +// ── Detail bottom sheet ─────────────────────────────────────────────────────── + +class _ClientDetailSheet extends StatefulWidget { + final Client client; + final void Function(String notes) onSaveNotes; + final void Function(double vol, int contracts, String provider, String notes) + onCompleteProfile; + + const _ClientDetailSheet({ + required this.client, + required this.onSaveNotes, + required this.onCompleteProfile, + }); + + @override + State<_ClientDetailSheet> createState() => _ClientDetailSheetState(); +} + +class _ClientDetailSheetState extends State<_ClientDetailSheet> { + late final TextEditingController _notesCtrl; + + // Complete-profile form (basicOnly clients) + bool _showCompleteForm = false; + double _upgradeVolume = 450; + final _upgradeContractsCtrl = TextEditingController(text: '12'); + String _upgradeProvider = 'DTDC Express'; + final _upgradeCustomProviderCtrl = TextEditingController(); + final _upgradeNotesCtrl = TextEditingController(); + + @override + void initState() { + super.initState(); + _notesCtrl = TextEditingController(text: widget.client.notes); + } + + @override + void dispose() { + _notesCtrl.dispose(); + _upgradeContractsCtrl.dispose(); + _upgradeCustomProviderCtrl.dispose(); + _upgradeNotesCtrl.dispose(); + super.dispose(); + } + + Color get _typeColor { + switch (widget.client.businessType) { + case BusinessType.ecommerce: + return const Color(0xFF930013); + case BusinessType.manufacturing: + return const Color(0xFF00504A); + case BusinessType.wholesale: + return const Color(0xFF3C475A); + case BusinessType.retail: + return const Color(0xFF545F73); + } + } + + Color get _typeBg { + switch (widget.client.businessType) { + case BusinessType.ecommerce: + return const Color(0xFFFFDAD7); + case BusinessType.manufacturing: + return const Color(0xFFD8F9EE); + case BusinessType.wholesale: + return const Color(0xFFD8E3FB); + case BusinessType.retail: + return const Color(0xFFEFF4FF); + } + } + + @override + Widget build(BuildContext context) { + final client = widget.client; + final isBasic = client.dataConsent == DataConsent.basicOnly; + + return DraggableScrollableSheet( + initialChildSize: 0.88, + minChildSize: 0.5, + maxChildSize: 0.97, + builder: (_, scrollCtrl) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.muted.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 12), + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row(children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: _typeBg, + borderRadius: BorderRadius.circular(4)), + child: Text( + client.businessType.label.toUpperCase(), + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w800, + letterSpacing: 1, + color: _typeColor), + ), + ), + if (isBasic) ...[ + const SizedBox(width: 6), + const ConsentBadge(), + ], + ]), + const SizedBox(height: 6), + Text(client.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + ], + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFF8F9FF), shape: BoxShape.circle), + child: Icon(Icons.close, + size: 16, color: AppColors.textSecondary), + ), + ), + ]), + ), + Divider( + height: 20, + color: AppColors.border.withValues(alpha: 0.15)), + Expanded( + child: ListView( + controller: scrollCtrl, + padding: + const EdgeInsets.symmetric(horizontal: 20), + children: [ + // Partial info banner + if (isBasic) _buildBasicOnlyBanner(), + + // ── Contact & Identity ─────────────────────────────── + _sectionLabel('CONTACT & IDENTITY'), + const SizedBox(height: 8), + Row(children: [ + Expanded( + child: _InfoTile( + icon: Icons.location_city_outlined, + label: 'CITY', + value: client.city.isNotEmpty + ? client.city + : '—')), + const SizedBox(width: 10), + Expanded( + child: _InfoTile( + icon: Icons.map_outlined, + label: 'NEIGHBOURHOOD', + value: client.neighbourhood.isNotEmpty + ? client.neighbourhood + : '—')), + ]), + const SizedBox(height: 10), + Row(children: [ + Expanded( + child: _InfoTile( + icon: Icons.phone_outlined, + label: 'PHONE', + value: client.phone.isNotEmpty + ? client.phone + : '—')), + const SizedBox(width: 10), + Expanded( + child: _InfoTile( + icon: Icons.repeat, + label: 'FREQUENCY', + value: client.frequency.isNotEmpty + ? client.frequency + : '—')), + ]), + if (client.logisticsSegment.isNotEmpty) ...[ + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: _InfoTile( + icon: client.logisticsSegment == 'First Mile' + ? Icons.warehouse_outlined + : client.logisticsSegment == 'Middle Mile' + ? Icons.local_shipping_outlined + : client.logisticsSegment == 'Last Mile' + ? Icons.markunread_mailbox_outlined + : Icons.category_outlined, + label: 'LOGISTICS SEGMENT', + value: client.logisticsSegment, + ), + ), + ], + ), + ], + + // ── Transit route ──────────────────────────────────── + if (client.transitFrom.isNotEmpty || + client.transitTo.isNotEmpty) ...[ + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(Icons.route, + size: 12, color: AppColors.primary), + const SizedBox(width: 4), + Text('USUAL TRANSIT ROUTE', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + ]), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _RouteStop( + icon: Icons.flight_takeoff, + label: 'FROM', + value: client.transitFrom.isNotEmpty + ? client.transitFrom + : '—'), + ), + Container( + width: 24, + height: 1, + margin: const EdgeInsets.only(top: 14, left: 8, right: 8), + color: AppColors.border.withValues(alpha: 0.3), + ), + Expanded( + child: _RouteStop( + icon: Icons.flight_land, + label: 'TO', + value: client.transitTo.isNotEmpty + ? client.transitTo + : '—'), + ), + ], + ), + ], + ), + ), + ], + + // ── Logistics details ──────────────────────────────── + const SizedBox(height: 14), + _sectionLabel('LOGISTICS DETAILS'), + const SizedBox(height: 8), + Row(children: [ + Expanded( + child: isBasic + ? _LockedTile(label: 'WEEKLY VOLUME') + : _InfoTile( + icon: Icons.inventory_2_outlined, + label: 'WEEKLY VOLUME', + value: '${client.parcelVolume.round()} /wk')), + const SizedBox(width: 10), + Expanded( + child: isBasic + ? _LockedTile(label: 'CONTRACTS') + : _InfoTile( + icon: Icons.description_outlined, + label: 'CONTRACTS', + value: + '${client.activeContracts} active')), + ]), + const SizedBox(height: 10), + + // Provider tile + if (isBasic) + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFFFF8ED), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFFFB800) + .withValues(alpha: 0.3)), + ), + child: Row(children: [ + const Icon(Icons.lock_outline, + size: 16, color: Color(0xFFE87C00)), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('LOGISTICS PROVIDER', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + const SizedBox(height: 4), + const Text('Not disclosed by client', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFFE87C00))), + ], + ), + ]), + ) + else + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('LOGISTICS PARTNER PROVIDER', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + const SizedBox(height: 4), + Text(client.provider, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFFFE4E6), + borderRadius: BorderRadius.circular(20), + ), + child: Text(client.efficiency, + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.w800, + color: AppColors.primary)), + ), + ], + ), + ), + const SizedBox(height: 10), + + // Status matrix + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(Icons.auto_awesome, + size: 12, color: AppColors.primary), + const SizedBox(width: 4), + Text('STATUS MATRIX', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + ]), + const SizedBox(height: 8), + _StatusRow( + label: 'Survey status', + value: 'SUBMITTED', + valueColor: AppColors.green), + _StatusRow( + label: 'Last checked', + value: client.lastUpdated), + ], + ), + ), + + // Complete profile section (basicOnly) + if (isBasic) ...[ + const SizedBox(height: 14), + _buildCompleteProfileSection(context), + ], + + // Notes (full clients only) + if (!isBasic) ...[ + const SizedBox(height: 14), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('CRM REMARKS', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + Text('Auto-saved', + style: TextStyle( + fontSize: 10, + color: AppColors.textSecondary)), + ], + ), + const SizedBox(height: 6), + TextField( + controller: _notesCtrl, + maxLines: 4, + decoration: const InputDecoration( + hintText: + 'e.g. Needs immediate review or transit optimisation…', + ), + ), + ], + const SizedBox(height: 80), + ], + ), + ), + + // Bottom action bar + Container( + padding: + const EdgeInsets.fromLTRB(16, 12, 16, 28), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + border: Border( + top: BorderSide( + color: AppColors.border.withValues(alpha: 0.15))), + ), + child: Row(children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + minimumSize: const Size.fromHeight(44), + ), + child: Text('Close', + style: TextStyle( + color: AppColors.textSecondary, + fontWeight: FontWeight.w700, + fontSize: 12)), + ), + ), + if (!isBasic) ...[ + const SizedBox(width: 10), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + widget.onSaveNotes(_notesCtrl.text); + Navigator.pop(context); + if (mounted) { + SnackbarUtils.showSuccess(context, 'Remarks saved!'); + } + }, + icon: const Icon(Icons.save, + size: 14, color: Colors.white), + label: const Text('Save Notes', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 12)), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12)), + minimumSize: const Size.fromHeight(44), + elevation: 1, + ), + ), + ), + ], + ]), + ), + ], + ), + ), + ); + } + + Widget _buildBasicOnlyBanner() { + return Container( + margin: const EdgeInsets.only(bottom: 14), + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFFFF8ED), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFFFB800).withValues(alpha: 0.5)), + ), + child: Row(children: [ + const Icon(Icons.lock_outline, + size: 16, color: Color(0xFFE87C00)), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Partial Information', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xFFE87C00))), + Text( + 'Commercial logistics details not yet shared. Use "Complete Profile" below.', + style: TextStyle( + fontSize: 11, color: AppColors.textSecondary), + ), + ], + ), + ), + ]), + ); + } + + Future _showUpgradeProviderSelectorSheet() async { + final picked = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ProviderPickerSheet( + initialProvider: _upgradeProvider, + ), + ); + if (picked != null) { + setState(() { + _upgradeProvider = picked; + }); + } + } + + Widget _buildCompleteProfileSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => + setState(() => _showCompleteForm = !_showCompleteForm), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF0B1C30), Color(0xFF1E3A5F)]), + borderRadius: BorderRadius.circular(14), + ), + child: Row(children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(10)), + child: Icon( + _showCompleteForm + ? Icons.keyboard_arrow_up + : Icons.add_circle_outline, + size: 20, + color: Colors.white, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Complete Profile', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Colors.white)), + Text( + _showCompleteForm + ? 'Tap to collapse' + : 'Collect volume, provider & notes', + style: TextStyle( + fontSize: 11, + color: Colors.white.withValues(alpha: 0.65)), + ), + ], + ), + ), + Icon( + _showCompleteForm + ? Icons.expand_less + : Icons.expand_more, + color: Colors.white.withValues(alpha: 0.7)), + ]), + ), + ), + if (_showCompleteForm) + Container( + margin: const EdgeInsets.only(top: 2), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(14)), + border: Border.all( + color: AppColors.border.withValues(alpha: 0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Volume + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Weekly Parcel Volume', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + Text('${_upgradeVolume.round()} /wk', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + fontFamily: 'monospace', + color: AppColors.primary)), + ], + ), + const SizedBox(height: 6), + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppColors.primary, + inactiveTrackColor: const Color(0xFFE8EEFF), + thumbColor: AppColors.primary, + overlayColor: AppColors.primary.withValues(alpha: 0.1), + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 9), + ), + child: Slider( + value: _upgradeVolume, + min: 50, + max: 1200, + divisions: 115, + onChanged: (v) => + setState(() => _upgradeVolume = v), + ), + ), + const SizedBox(height: 12), + + // Contracts + const Text('Active Service Contracts', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 6), + TextField( + controller: _upgradeContractsCtrl, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.description_outlined, + size: 16), + ), + ), + const SizedBox(height: 12), + + // Provider + const Text('Primary Service Provider', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 6), + InkWell( + onTap: _showUpgradeProviderSelectorSheet, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.border.withValues(alpha: 0.5)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + _upgradeProvider, + style: const TextStyle( + fontSize: 13, + color: AppColors.darkBlue, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Icon(Icons.arrow_drop_down, color: AppColors.textSecondary), + ], + ), + ), + ), + if (_upgradeProvider == 'Others') ...[ + const SizedBox(height: 12), + const Text('Specify Other Provider', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 6), + TextField( + controller: _upgradeCustomProviderCtrl, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + hintText: 'e.g. My Local Logistics Operator', + prefixIcon: Icon(Icons.edit_note, size: 18), + ), + ), + ], + const SizedBox(height: 12), + + // Notes + const Text('Survey Remarks', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 6), + TextField( + controller: _upgradeNotesCtrl, + maxLines: 3, + decoration: const InputDecoration( + hintText: 'Additional observations…', + ), + ), + const SizedBox(height: 16), + + SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton.icon( + onPressed: () { + String providerName = _upgradeProvider == 'Others' + ? _upgradeCustomProviderCtrl.text.trim() + : _upgradeProvider; + if (providerName.isEmpty) providerName = 'Others'; + + widget.onCompleteProfile( + _upgradeVolume, + int.tryParse( + _upgradeContractsCtrl.text) ?? + 12, + providerName, + _upgradeNotesCtrl.text.trim(), + ); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text( + 'Profile upgraded to full details!'), + backgroundColor: AppColors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10)), + ), + ); + }, + icon: const Icon(Icons.lock_open, + size: 14, color: Colors.white), + label: const Text('Save Full Profile', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700)), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0B1C30), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 1, + ), + ), + ), + ], + ), + ), + ], + ); + } + + Widget _sectionLabel(String text) => Text(text, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + color: AppColors.textSecondary)); +} + +// ── Info tiles ──────────────────────────────────────────────────────────────── + +class _InfoTile extends StatelessWidget { + final IconData icon; + final String label; + final String value; + const _InfoTile( + {required this.icon, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(icon, size: 10, color: AppColors.textSecondary), + const SizedBox(width: 4), + Text(label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + ]), + const SizedBox(height: 4), + Text(value, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ], + ), + ); + } +} + +class _LockedTile extends StatelessWidget { + final String label; + const _LockedTile({required this.label}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF8ED), + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: const Color(0xFFFFB800).withValues(alpha: 0.3)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + const SizedBox(height: 4), + Row(children: [ + const Icon(Icons.lock_outline, + size: 12, color: Color(0xFFE87C00)), + const SizedBox(width: 4), + const Text('Hidden', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: Color(0xFFE87C00))), + ]), + ], + ), + ); + } +} + +class _RouteStop extends StatelessWidget { + final IconData icon; + final String label; + final String value; + const _RouteStop( + {required this.icon, required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(icon, size: 10, color: AppColors.primary), + const SizedBox(width: 3), + Text(label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: AppColors.textSecondary)), + ]), + const SizedBox(height: 2), + Text(value, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + ], + ); + } +} + +class _StatusRow extends StatelessWidget { + final String label; + final String value; + final Color? valueColor; + const _StatusRow( + {required this.label, required this.value, this.valueColor}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: TextStyle( + fontSize: 11, color: AppColors.textSecondary)), + Text(value, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: valueColor ?? AppColors.darkBlue)), + ], + ), + ); + } +} diff --git a/lib/screens/dashboard_screen.dart b/lib/screens/dashboard_screen.dart new file mode 100644 index 0000000..3126de9 --- /dev/null +++ b/lib/screens/dashboard_screen.dart @@ -0,0 +1,709 @@ +import 'package:flutter/material.dart'; +import '../models/client.dart'; +import '../models/activity.dart'; +import '../models/app_stats.dart'; +import '../theme/app_theme.dart'; +import 'surveys_list_screen.dart'; + +class DashboardScreen extends StatefulWidget { + final AppStats stats; + final List activities; + final List clients; + final List surveys; + final List pricing; + final void Function(int) onNavigate; + final VoidCallback onDataChanged; + final bool backendReady; + final VoidCallback onLogout; + final bool isActive; + + const DashboardScreen({ + super.key, + required this.stats, + required this.activities, + required this.clients, + this.surveys = const [], + this.pricing = const [], + required this.onNavigate, + required this.onDataChanged, + required this.backendReady, + required this.onLogout, + this.isActive = false, + }); + + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void didUpdateWidget(covariant DashboardScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isActive && !oldWidget.isActive) { + if (_scrollController.hasClients) { + _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.fromLTRB(16, 20, 16, 100), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WideTopBanner( + backendReady: widget.backendReady, + subtitle: 'DASHBOARD', + onLogout: widget.onLogout, + ), + const SizedBox(height: 20), + _WelcomeHeader(), + const SizedBox(height: 20), + _BentoGrid(stats: widget.stats), + const SizedBox(height: 20), + _QuickActions(onNavigate: widget.onNavigate), + const SizedBox(height: 20), + _MarketInsights(surveys: widget.surveys, pricing: widget.pricing, onNavigate: widget.onNavigate, onDataChanged: widget.onDataChanged), + const SizedBox(height: 20), + _RecentActivity( + activities: widget.activities, + clients: widget.clients, + onNavigate: widget.onNavigate, + ), + const SizedBox(height: 20), + _InsightCard(), + ], + ), + ); + } +} + +class _WelcomeHeader extends StatelessWidget { + static String get _greeting { + final h = DateTime.now().hour; + if (h < 12) return 'Good Morning'; + if (h < 17) return 'Good Afternoon'; + if (h < 21) return 'Good Evening'; + return 'Good Night'; + } + + static String get _subtext { + final h = DateTime.now().hour; + if (h < 12) return 'Start your day with a fresh logistics survey.'; + if (h < 17) return 'Keep the momentum log your field visits.'; + if (h < 21) return 'Wrap up the day any pending surveys to log?'; + return 'Late session active your data is synced.'; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _greeting, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + color: AppColors.darkBlue, + ), + ), + const SizedBox(height: 4), + Text( + _subtext, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ); + } +} + +class _BentoGrid extends StatelessWidget { + final AppStats stats; + const _BentoGrid({required this.stats}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Large card spanning full width + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFFD3E4FE), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFA6C8FF).withValues(alpha: 0.3)), + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'TOTAL CLIENTS', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: AppColors.primary, + ), + ), + const SizedBox(height: 8), + Text( + stats.totalClients.toString().replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (m) => '${m[1]},', + ), + style: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.w900, + color: AppColors.darkBlue, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Icon(Icons.arrow_upward, + size: 14, color: AppColors.greenDark), + const SizedBox(width: 4), + Text( + '+12% from last month', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppColors.greenDark, + ), + ), + ], + ), + ], + ), + Positioned( + right: -20, + bottom: -20, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primary.withValues(alpha: 0.06), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _SmallStatCard( + icon: Icons.calendar_today, + iconColor: AppColors.primary, + value: stats.todayEntries.toString(), + label: "Today's Entries", + ), + ), + const SizedBox(width: 12), + Expanded( + child: _SmallStatCard( + icon: Icons.access_time, + iconColor: const Color(0xFF004D47), + value: stats.pendingFollowups.toString(), + label: 'Pending Follow-ups', + ), + ), + ], + ), + ], + ); + } +} + +class _SmallStatCard extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String value; + final String label; + + const _SmallStatCard({ + required this.icon, + required this.iconColor, + required this.value, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border.withValues(alpha: 0.35)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 24, color: iconColor), + const SizedBox(height: 10), + Text( + value, + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary, + ), + ), + ], + ), + ); + } +} + +class _QuickActions extends StatelessWidget { + final void Function(int) onNavigate; + const _QuickActions({required this.onNavigate}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'QUICK ACTIONS', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton.icon( + onPressed: () => onNavigate(4), + icon: const Icon(Icons.add, size: 18, color: Colors.white), + label: const Text( + 'Quick Add Client', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + fontSize: 14, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + elevation: 1, + ), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton.icon( + onPressed: () => onNavigate(1), + icon: Icon(Icons.search, size: 18, color: AppColors.darkBlue), + label: Text( + 'Search Client Database', + style: TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.darkBlue, + fontSize: 14, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD3E4FE), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: const BorderSide(color: Color(0xFFA6C8FF)), + ), + elevation: 0, + ), + ), + ), + ], + ); + } +} + +class _MarketInsights extends StatelessWidget { + final List surveys; + final List pricing; + final void Function(int) onNavigate; + final VoidCallback onDataChanged; + + const _MarketInsights({required this.surveys, required this.pricing, required this.onNavigate, required this.onDataChanged}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'MARKET DATA', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute( + builder: (ctx) => SurveysListScreen( + surveys: surveys, + onDataChanged: onDataChanged, + ), + )); + }, + child: _SmallStatCard( + icon: Icons.storefront_outlined, + iconColor: const Color(0xFFE87C00), + value: surveys.length.toString(), + label: 'Competitor Surveys', + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => onNavigate(3), + child: _SmallStatCard( + icon: Icons.price_change_outlined, + iconColor: const Color(0xFF15803D), + value: pricing.length.toString(), + label: 'Carrier Pricing', + ), + ), + ), + ], + ), + ], + ); + } +} + +class _RecentActivity extends StatelessWidget { + final List activities; + final List clients; + final void Function(int) onNavigate; + + const _RecentActivity({ + required this.activities, + required this.clients, + required this.onNavigate, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'RECENT ACTIVITY', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: AppColors.textSecondary, + ), + ), + GestureDetector( + onTap: () => onNavigate(1), + child: Text( + 'View all', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.primary, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border.withValues(alpha: 0.2)), + ), + child: Column( + children: activities + .take(5) + .map((activity) => _ActivityTile( + activity: activity, + onTap: () => onNavigate(1), + isLast: activity == activities.last, + )) + .toList(), + ), + ), + ], + ); + } +} + +class _ActivityTile extends StatelessWidget { + final Activity activity; + final VoidCallback onTap; + final bool isLast; + + const _ActivityTile({ + required this.activity, + required this.onTap, + required this.isLast, + }); + + @override + Widget build(BuildContext context) { + Color iconBg; + Color iconColor; + IconData iconData; + + switch (activity.icon) { + case ActivityIcon.shipping: + iconBg = const Color(0xFFD5E0F8); + iconColor = const Color(0xFF1E3A8A); + iconData = Icons.local_shipping; + break; + case ActivityIcon.call: + iconBg = const Color(0xFFFFE4E6); + iconColor = AppColors.primary; + iconData = Icons.phone; + break; + case ActivityIcon.edit: + iconBg = const Color(0xFFDCFCE7); + iconColor = const Color(0xFF15803D); + iconData = Icons.edit; + break; + } + + Widget? badge; + if (activity.status == ActivityStatus.newStatus) { + badge = _Badge(label: 'NEW', bg: const Color(0xFFFFDAD7), fg: const Color(0xFF930013)); + } else if (activity.status == ActivityStatus.pending) { + badge = _Badge(label: 'PENDING', bg: const Color(0xFFD8E3FB), fg: const Color(0xFF3C475A)); + } else if (activity.status == ActivityStatus.completed) { + badge = _Badge(label: 'DONE', bg: const Color(0xFFD8F9EE), fg: const Color(0xFF115E59)); + } + + return InkWell( + onTap: onTap, + borderRadius: isLast + ? const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ) + : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + border: isLast + ? null + : Border( + bottom: BorderSide( + color: AppColors.border.withValues(alpha: 0.15), + ), + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: iconBg, + shape: BoxShape.circle, + ), + child: Icon(iconData, size: 18, color: iconColor), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activity.clientName, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + activity.action, + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + ?badge, + ], + ), + ), + ); + } +} + +class _Badge extends StatelessWidget { + final String label; + final Color bg; + final Color fg; + + const _Badge({required this.label, required this.bg, required this.fg}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: fg, + ), + ), + ); + } +} + +class _InsightCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: SizedBox( + width: double.infinity, + height: 160, + child: Stack( + fit: StackFit.expand, + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF0B1C30), Color(0xFF8D0012)], + ), + ), + ), + Positioned.fill( + child: CustomPaint(painter: _GridPainter()), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + Icon(Icons.auto_awesome, + size: 12, color: const Color(0xFFFFDAD7)), + const SizedBox(width: 4), + Text( + 'INSIGHT OF THE WEEK', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: const Color(0xFFFFDAD7), + ), + ), + ], + ), + const SizedBox(height: 6), + const Text( + 'Weekly Performance Report', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + "Analyze your team's survey conversion rates.", + style: TextStyle( + fontSize: 11, + color: Colors.white.withValues(alpha: 0.8), + fontWeight: FontWeight.w300, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _GridPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withValues(alpha: 0.04) + ..strokeWidth = 1; + const spacing = 30.0; + for (double x = 0; x < size.width; x += spacing) { + canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); + } + for (double y = 0; y < size.height; y += spacing) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(_GridPainter oldDelegate) => false; +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart new file mode 100644 index 0000000..264958c --- /dev/null +++ b/lib/screens/login_screen.dart @@ -0,0 +1,266 @@ +import 'package:flutter/material.dart'; +import '../services/auth_service.dart'; +import '../theme/app_theme.dart'; +import '../utils/snackbar_utils.dart'; + +class LoginScreen extends StatefulWidget { + final void Function(AuthUser user) onLoggedIn; + + const LoginScreen({super.key, required this.onLoggedIn}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _auth = AuthService(); + + final _emailCtrl = TextEditingController(); + final _passwordCtrl = TextEditingController(); + + bool _busy = false; + + @override + void dispose() { + _emailCtrl.dispose(); + _passwordCtrl.dispose(); + super.dispose(); + } + + Future _login() async { + final email = _emailCtrl.text.trim(); + final password = _passwordCtrl.text; + + if (email.isEmpty || password.isEmpty) { + SnackbarUtils.showError(context, 'Enter your email and password'); + return; + } + + setState(() { + _busy = true; + }); + + try { + final user = await _auth.login(email, password); + if (!mounted) return; + if (user == null) { + setState(() { + _busy = false; + }); + SnackbarUtils.showError(context, 'Invalid email or password'); + return; + } + + widget.onLoggedIn(user); + } catch (_) { + if (!mounted) return; + setState(() { + _busy = false; + }); + SnackbarUtils.showError(context, 'Could not reach server. Check your connection.'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.primary, + body: SafeArea( + child: Align( + alignment: const Alignment(0, -0.4), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _logo(), + const SizedBox(height: 20), + const Text( + 'Doormile CRM', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 0.2, + ), + ), + const SizedBox(height: 6), + Text( + 'FIELD SALES PORTAL', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 2, + color: Colors.white.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 28), + _card(), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _logo() { + return Container( + width: 92, + height: 92, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 24, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(14), + child: Image.asset('assets/doormilelogoround.png', fit: BoxFit.contain), + ), + ); + } + + Widget _card() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(22), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 30, + offset: const Offset(0, 12), + ), + ], + ), + child: _loginForm(), + ); + } + + Widget _loginForm() { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Sign In', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue, + ), + ), + const SizedBox(height: 6), + const Text( + 'Welcome back! Please enter your details.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13, color: AppColors.textSecondary), + ), + const SizedBox(height: 24), + TextField( + controller: _emailCtrl, + keyboardType: TextInputType.emailAddress, + autocorrect: false, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + hintText: 'Email', + prefixIcon: const Icon(Icons.email_outlined, color: AppColors.muted), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary, width: 2), + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordCtrl, + obscureText: true, + textInputAction: TextInputAction.go, + onSubmitted: (_) => _login(), + decoration: InputDecoration( + hintText: 'Password', + prefixIcon: const Icon(Icons.lock_outline, color: AppColors.muted), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary, width: 2), + ), + ), + ), + const SizedBox(height: 24), + _primaryButton( + label: 'Continue', + onPressed: _busy ? null : _login, + ), + ], + ); + } + Widget _primaryButton({ + required String label, + required VoidCallback? onPressed, + }) { + return SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.5), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _busy + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2.2, + color: Colors.white, + ), + ) + : Text( + label, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + ), + ), + ), + ); + } +} diff --git a/lib/screens/pricing_screen.dart b/lib/screens/pricing_screen.dart new file mode 100644 index 0000000..9cf3fca --- /dev/null +++ b/lib/screens/pricing_screen.dart @@ -0,0 +1,538 @@ +import 'package:flutter/material.dart'; +import '../services/crm_api_service.dart'; +import '../theme/app_theme.dart'; +import '../utils/snackbar_utils.dart'; + +class PricingScreen extends StatefulWidget { + final List pricing; + final bool backendReady; + final bool isActive; + final VoidCallback onDataChanged; + + const PricingScreen({ + super.key, + required this.pricing, + required this.backendReady, + required this.onDataChanged, + this.isActive = false, + }); + + @override + State createState() => _PricingScreenState(); +} + +class _PricingScreenState extends State { + final CrmApiService _api = CrmApiService(); + + bool _isSaving = false; + + // Group pricing by company + Map> get groupedPricing { + final Map> map = {}; + for (var p in widget.pricing) { + final company = p['company']?.toString() ?? 'Unknown'; + if (!map.containsKey(company)) { + map[company] = []; + } + map[company]!.add(p); + } + return map; + } + + void _showAddPricingDialog([String? defaultProvider]) { + _showPricingFormDialog(defaultProvider: defaultProvider); + } + + void _showEditPricingDialog(Map rateData) { + _showPricingFormDialog(rateData: rateData); + } + + void _showPricingFormDialog({Map? rateData, String? defaultProvider}) { + final isEditing = rateData != null; + final companyCtrl = TextEditingController(text: rateData?['company']?.toString() ?? defaultProvider ?? ''); + final slabCtrl = TextEditingController(text: rateData?['weight_slab']?.toString() ?? ''); + final zoneCtrl = TextEditingController(text: rateData?['zone']?.toString().isNotEmpty == true ? rateData!['zone'] : rateData?['service_type'] ?? ''); + final rateCtrl = TextEditingController(text: rateData?['rate']?.toString() ?? ''); + final String id = rateData?['id']?.toString() ?? ''; + + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 20), + decoration: const BoxDecoration( + color: AppColors.blueLight, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Icon(isEditing ? Icons.edit_outlined : Icons.add_circle_outline, color: AppColors.primary, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(isEditing ? 'Edit Rate' : 'Add New Rate', style: const TextStyle(fontWeight: FontWeight.w800, color: AppColors.darkBlue, fontSize: 18)), + const SizedBox(height: 2), + Text(isEditing ? 'Update pricing information' : 'Add to provider pricing slab', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ), + // Form Fields + Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + _buildModernTextField( + controller: companyCtrl, + label: 'Provider Name', + hint: 'e.g. DTDC, Delhivery', + icon: Icons.storefront_outlined, + ), + const SizedBox(height: 16), + _buildModernTextField( + controller: slabCtrl, + label: 'Weight Slab', + hint: 'e.g. 500g, 1kg-2kg', + icon: Icons.scale_outlined, + ), + const SizedBox(height: 16), + _buildModernTextField( + controller: zoneCtrl, + label: 'Zone / Service', + hint: 'e.g. LOCAL, SOUTH', + icon: Icons.place_outlined, + ), + const SizedBox(height: 16), + _buildModernTextField( + controller: rateCtrl, + label: 'Rate (₹)', + hint: '0.00', + icon: Icons.currency_rupee, + keyboardType: TextInputType.number, + ), + ], + ), + ), + // Actions + Container( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(ctx), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Cancel', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + if (companyCtrl.text.isEmpty || slabCtrl.text.isEmpty || rateCtrl.text.isEmpty) { + SnackbarUtils.showError(ctx, 'Please fill in required fields'); + return; + } + + setState(() => _isSaving = true); + Navigator.pop(ctx); + + final payload = { + 'company': companyCtrl.text, + 'weight_slab': slabCtrl.text, + 'zone': zoneCtrl.text, + 'service_type': '', + 'rate': rateCtrl.text, + }; + + final success = isEditing + ? await _api.updatePricing(id, payload) + : await _api.createPricing(payload); + + setState(() => _isSaving = false); + + if (success) { + widget.onDataChanged(); + if (mounted) { + SnackbarUtils.showSuccess(context, isEditing ? 'Rate updated!' : 'Rate added!'); + } + } else { + if (mounted) { + SnackbarUtils.showError(context, isEditing ? 'Update failed.' : 'Failed to add rate.'); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text(isEditing ? 'Save Changes' : 'Add Rate', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildModernTextField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + TextInputType keyboardType = TextInputType.text, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: AppColors.darkBlue, letterSpacing: 0.5), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + keyboardType: keyboardType, + style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.darkBlue, fontSize: 14), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: AppColors.textSecondary.withValues(alpha: 0.5)), + prefixIcon: Icon(icon, size: 18, color: AppColors.textSecondary), + filled: true, + fillColor: const Color(0xFFF8F9FA), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary), + ), + ), + ), + ], + ); + } + + void _deletePricing(String id) async { + final confirm = await showDialog( + context: context, + builder: (ctx) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: AppColors.primary.withValues(alpha: 0.1), shape: BoxShape.circle), + child: const Icon(Icons.warning_amber_rounded, color: AppColors.primary, size: 32), + ), + const SizedBox(height: 16), + const Text('Delete Pricing Rate?', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), + const SizedBox(height: 8), + const Text('This action cannot be undone. Are you sure you want to delete this rate?', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: AppColors.textSecondary)), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(ctx, false), + style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: const Text('Cancel', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Delete', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + + if (confirm != true) return; + + setState(() => _isSaving = true); + final success = await _api.deletePricing(id); + setState(() => _isSaving = false); + + if (success) { + widget.onDataChanged(); + if (mounted) { + SnackbarUtils.showSuccess(context, 'Pricing rate deleted.'); + } + } else { + if (mounted) { + SnackbarUtils.showError(context, 'Failed to delete pricing rate.'); + } + } + } + + @override + Widget build(BuildContext context) { + final groups = groupedPricing; + final companies = groups.keys.toList()..sort(); + + return Scaffold( + backgroundColor: AppColors.scaffold, + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 10), + child: WideTopBanner( + backendReady: widget.backendReady, + subtitle: 'CARRIER PRICING', + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Providers', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue, + ), + ), + ElevatedButton.icon( + onPressed: _isSaving ? null : () => _showAddPricingDialog(), + icon: const Icon(Icons.add, size: 18, color: Colors.white), + label: const Text('Add Provider / Rate', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ], + ), + ), + ), + if (_isSaving) + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Center(child: CircularProgressIndicator()), + ), + ), + if (companies.isEmpty && !_isSaving) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Center( + child: Text( + 'No pricing data available.\nAdd a provider to get started.', + textAlign: TextAlign.center, + style: TextStyle(color: AppColors.textSecondary, fontSize: 16), + ), + ), + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final company = companies[index]; + final rates = groups[company] ?? []; + + final zones = rates.map((r) => r['zone']?.toString().toUpperCase() ?? 'LOCAL').toSet().toList()..sort(); + final slabs = rates.map((r) => r['weight_slab']?.toString() ?? '').toSet().toList()..sort(); + final initial = company.isNotEmpty ? company.substring(0, 1).toUpperCase() : 'P'; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200, width: 1), + ), + elevation: 0, + color: Colors.white, + child: ExpansionTile( + shape: const Border(), + tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFFDECEE), + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: Text( + initial, + style: const TextStyle(color: AppColors.primary, fontSize: 20, fontWeight: FontWeight.w800), + ), + ), + title: Text( + company, + style: const TextStyle(fontWeight: FontWeight.w800, color: AppColors.darkBlue, fontSize: 16), + ), + subtitle: Row( + children: [ + const Icon(Icons.place_outlined, size: 14, color: AppColors.primary), + const SizedBox(width: 4), + Text('${rates.length} Registered Rates', style: const TextStyle(color: AppColors.textSecondary, fontSize: 13)), + ], + ), + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade200)), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + headingRowColor: WidgetStateProperty.all(Colors.grey.shade50), + dataRowMinHeight: 56, + dataRowMaxHeight: 56, + columnSpacing: 32, + horizontalMargin: 24, + columns: [ + const DataColumn(label: Text('WEIGHT SLAB', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: AppColors.textSecondary))), + ...zones.map((z) => DataColumn(label: Text(z, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: AppColors.textSecondary)))), + ], + rows: slabs.map((slab) { + return DataRow( + cells: [ + DataCell(Text(slab, style: const TextStyle(fontWeight: FontWeight.w700, color: AppColors.darkBlue, fontSize: 13))), + ...zones.map((zone) { + final matchingRate = rates.firstWhere( + (r) => (r['weight_slab'] == slab) && ((r['zone']?.toString().toUpperCase() ?? 'LOCAL') == zone), + orElse: () => null, + ); + if (matchingRate == null) { + return const DataCell(Center(child: Text('—', style: TextStyle(color: Colors.grey)))); + } + return DataCell( + InkWell( + onTap: () { + // Show options to edit or delete + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.edit_outlined, color: AppColors.textSecondary), + title: const Text('Edit Rate'), + onTap: () { + Navigator.pop(ctx); + _showEditPricingDialog(matchingRate as Map); + }, + ), + ListTile( + leading: const Icon(Icons.delete_outline, color: AppColors.primary), + title: const Text('Delete Rate', style: TextStyle(color: AppColors.primary)), + onTap: () { + Navigator.pop(ctx); + _deletePricing(matchingRate['id'].toString()); + }, + ), + ], + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: AppColors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '₹${matchingRate['rate']}', + style: const TextStyle(fontWeight: FontWeight.w800, color: AppColors.greenDark, fontSize: 14), + ), + ), + ), + ); + }), + ], + ); + }).toList(), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: () => _showAddPricingDialog(company), + icon: const Icon(Icons.add, size: 16, color: AppColors.primary), + label: const Text('Add Rate', style: TextStyle(color: AppColors.primary, fontWeight: FontWeight.w700)), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + childCount: companies.length, + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], + ), + ); + } +} diff --git a/lib/screens/survey_screen.dart b/lib/screens/survey_screen.dart new file mode 100644 index 0000000..3799b83 --- /dev/null +++ b/lib/screens/survey_screen.dart @@ -0,0 +1,2843 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; +import '../data/india_cities.dart'; +import '../data/india_locations.dart'; +import '../models/client.dart'; +import '../services/location_service.dart'; +import '../theme/app_theme.dart'; +import '../utils/snackbar_utils.dart'; + +class SurveyScreen extends StatefulWidget { + final List clients; + final void Function(Client) onAddClient; + final void Function(int) onNavigate; + final bool backendReady; + final bool isActive; + + const SurveyScreen({ + super.key, + required this.clients, + required this.onAddClient, + required this.onNavigate, + required this.backendReady, + this.isActive = false, + }); + + @override + State createState() => _SurveyScreenState(); +} + +enum _SurveyState { idle, surveying, success } + +class _SurveyScreenState extends State + with SingleTickerProviderStateMixin { + _SurveyState _state = _SurveyState.idle; + + int _step = 1; + bool _onConsentStep = false; + bool _capturingLocation = false; + DataConsent _chosenConsent = DataConsent.full; + + // Step 1 — basic identity + contact + routing + final _nameCtrl = TextEditingController(); + final _phoneCtrl = TextEditingController(); + final _neighbourhoodCtrl = TextEditingController(); + final _pincodeCtrl = TextEditingController(); + final _customProviderCtrl = TextEditingController(); + IndiaCity? _businessCity; // origin — auto-detected from GPS or picked + List _destCities = []; // transiting destination cities (multi-select) + SurveyLocation? _capturedLocation; // GPS captured during Step 1 (reused at submit) + final bool _detectingLocation = false; + String _frequency = ''; + BusinessType _businessType = BusinessType.ecommerce; + List _selectedSegments = ['First Mile']; + + // Step 2 — logistics metrics + double _parcelVolume = 450; + final _contractsCtrl = TextEditingController(text: '12'); + + // Step 3 — provider & notes + String _provider = 'DTDC Express'; + final _notesCtrl = TextEditingController(); + + late AnimationController _animCtrl; + late Animation _fadeAnim; + final ScrollController _scrollController = ScrollController(); + + static const _frequencyOptions = [ + 'Daily', + 'Weekly', + 'Bi-weekly', + 'Monthly', + 'On-demand', + ]; + + @override + void initState() { + super.initState(); + _animCtrl = AnimationController( + vsync: this, duration: const Duration(milliseconds: 300)); + _fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut); + _animCtrl.forward(); + } + + @override + void didUpdateWidget(covariant SurveyScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isActive && !oldWidget.isActive) { + if (_scrollController.hasClients) { + _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + } + } + + @override + void dispose() { + _scrollController.dispose(); + _nameCtrl.dispose(); + _phoneCtrl.dispose(); + _neighbourhoodCtrl.dispose(); + _pincodeCtrl.dispose(); + _customProviderCtrl.dispose(); + _contractsCtrl.dispose(); + _notesCtrl.dispose(); + _animCtrl.dispose(); + super.dispose(); + } + + List get _lastSurveyed => + widget.clients.where((c) => c.surveySubmitted).take(3).toList(); + + + + /// Average parcel volume across clients with the same business type. + int get _avgVolumeForType { + final peers = widget.clients + .where((c) => c.businessType == _businessType && c.parcelVolume > 0) + .toList(); + if (peers.isEmpty) return 0; + return (peers.fold(0.0, (s, c) => s + c.parcelVolume) / peers.length).round(); + } + + // ── Location ────────────────────────────────────────────────────────────── + + Future _showProminentLocationDisclosure() async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: const BoxDecoration( + color: AppColors.blueLight, + shape: BoxShape.circle, + ), + child: const Icon(Icons.location_on, size: 28, color: AppColors.primary), + ), + const SizedBox(height: 16), + const Text( + 'Location Disclosure', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue, + ), + ), + const SizedBox(height: 8), + const Text( + 'DoorMile CRM collects location data to record the GPS coordinates of your client visit when you submit a survey, enabling verification of field visits. This data is collected only when you explicitly press the submit survey button, even when the app is in use.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.5, + color: AppColors.textSecondary, + height: 1.4, + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + 'Agree & Continue', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 44, + child: TextButton( + onPressed: () => Navigator.pop(ctx, false), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle( + fontSize: 13, + color: AppColors.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ); + return result ?? false; + } + + /// Checks service + permission (showing alerts as needed), then captures GPS. + /// Returns [SurveyLocation.empty] if unavailable / declined. + Future _acquireLocation() async { + final status = await LocationService.checkStatus(); + if (!mounted) return SurveyLocation.empty(); + + if (status == LocationCheckResult.serviceDisabled) { + final proceed = await _showLocationAlert( + icon: Icons.location_off_outlined, + title: 'Location is Turned Off', + message: + 'Enable location on your device so the client visit can be ' + 'logged with GPS coordinates.', + primaryLabel: 'Turn On Location', + onPrimary: LocationService.openLocationSettings, + ); + if (!proceed || !mounted) return SurveyLocation.empty(); + } else if (status == LocationCheckResult.permissionDenied) { + final agreed = await _showProminentLocationDisclosure(); + if (!agreed || !mounted) return SurveyLocation.empty(); + + final perm = await Geolocator.requestPermission(); + if (!mounted) return SurveyLocation.empty(); + if (perm == LocationPermission.denied || + perm == LocationPermission.deniedForever) { + final proceed = await _showLocationAlert( + icon: Icons.lock_outline, + title: 'Location Permission Needed', + message: + 'Allow DoorMile to access your location so the business ' + 'location can be detected automatically.', + primaryLabel: 'Open App Settings', + onPrimary: LocationService.openAppSettings, + ); + if (!proceed || !mounted) return SurveyLocation.empty(); + } + } else if (status == LocationCheckResult.permissionPermanentlyDenied) { + final proceed = await _showLocationAlert( + icon: Icons.lock_outline, + title: 'Location Permission Blocked', + message: + 'Location access was blocked. Open App Settings and allow ' + '"Location" for DoorMile to detect the business location.', + primaryLabel: 'Open App Settings', + onPrimary: LocationService.openAppSettings, + ); + if (!proceed || !mounted) return SurveyLocation.empty(); + } + + setState(() => _capturingLocation = true); + SurveyLocation loc = SurveyLocation.empty(); + try { + loc = await LocationService.capture(); + } catch (_) {} + if (!mounted) return loc; + setState(() => _capturingLocation = false); + return loc; + } + + /// Step 1 action — detect the business location from the device GPS and + /// auto-fill the business city (and neighbourhood, if empty). + Future _detectLocation() async { + final loc = await _acquireLocation(); + if (!mounted) return; + if (!loc.hasFix) { + SnackbarUtils.showError(context, "Couldn't get GPS. Pick the business city manually below."); + return; + } + setState(() { + _capturedLocation = loc; + final matched = findCityByName(loc.city); + _businessCity = matched ?? + (loc.city.isNotEmpty + ? IndiaCity(loc.city, '—', loc.state) + : _businessCity); + if (loc.zone.isNotEmpty && _neighbourhoodCtrl.text.trim().isEmpty) { + _neighbourhoodCtrl.text = loc.zone; + } + // Pre-fill the optional pincode from GPS, if the agent hasn't typed one. + if (loc.pincode.isNotEmpty && _pincodeCtrl.text.trim().isEmpty) { + _pincodeCtrl.text = loc.pincode; + } + }); + } + + /// Submit entry point — reuses the GPS captured during Step 1 if available, + /// otherwise acquires it now. + Future _captureAndSubmit({required bool basicOnly}) async { + if (!basicOnly && _nameCtrl.text.trim().isEmpty) return; + + SurveyLocation loc; + if (_capturedLocation != null && _capturedLocation!.hasFix) { + loc = _capturedLocation!; + } else { + loc = await _acquireLocation(); + if (!mounted) return; + } + + if (basicOnly) { + _doSubmitBasicOnly(loc); + } else { + _doSubmitFull(loc); + } + } + + /// Shows a styled location alert. + /// + /// Returns `true` → user chose "Submit Without Location" (proceed). + /// Returns `false` → user chose the settings action (don't submit now). + Future _showLocationAlert({ + required IconData icon, + required String title, + required String message, + required String primaryLabel, + required Future Function() onPrimary, + }) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: const Color(0xFFFFF8ED), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 26, color: const Color(0xFFE87C00)), + ), + const SizedBox(height: 16), + Text(title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 8), + Text(message, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, color: AppColors.textSecondary)), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton( + onPressed: () { + Navigator.pop(ctx, false); + onPrimary(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 0, + ), + child: Text(primaryLabel, + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 44, + child: TextButton( + onPressed: () => Navigator.pop(ctx, true), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: Text('Submit Without Location', + style: TextStyle( + fontSize: 13, + color: AppColors.textSecondary, + fontWeight: FontWeight.w600)), + ), + ), + ], + ), + ), + ), + ); + return result ?? true; + } + + void _doSubmitBasicOnly(SurveyLocation loc) { + _chosenConsent = DataConsent.basicOnly; + widget.onAddClient(Client( + id: 'client_${DateTime.now().millisecondsSinceEpoch}', + name: _nameCtrl.text.trim(), + city: _businessCity?.name ?? '', + businessType: _businessType, + parcelVolume: 0.0, + activeContracts: 0, + provider: 'Not disclosed', + efficiency: 'Not disclosed', + status: ClientStatus.newClient, + lastUpdated: 'Just now', + surveySubmitted: true, + notes: '', + dataConsent: DataConsent.basicOnly, + phone: _phoneCtrl.text.trim(), + frequency: _frequency, + businessState: _businessCity?.state ?? '', + transitType: '', + transitFrom: _businessCity?.name ?? '', + transitTo: _destCities.map((c) => c.name).join(', '), + neighbourhood: _neighbourhoodCtrl.text.trim(), + logisticsSegment: _selectedSegments.join(', '), + pincode: _pincodeCtrl.text.trim(), + surveyLat: loc.lat, + surveyLng: loc.lng, + surveyAddress: loc.address, + surveyZone: loc.zone, + surveyPincode: loc.pincode, + )); + _transition(_SurveyState.success); + } + + void _doSubmitFull(SurveyLocation loc) { + _chosenConsent = DataConsent.full; + String providerName = _provider == 'Others' ? _customProviderCtrl.text.trim() : _provider; + if (providerName.isEmpty) providerName = 'Others'; + + String efficiency = 'High Efficiency'; + if (providerName == 'Blue Dart') efficiency = 'Premium Service'; + if (providerName == 'Gati-KWE') efficiency = 'Cost Leader'; + if (providerName == 'Delhivery') efficiency = 'Local Last Mile'; + + widget.onAddClient(Client( + id: 'client_${DateTime.now().millisecondsSinceEpoch}', + name: _nameCtrl.text.trim(), + city: _businessCity?.name ?? '', + businessType: _businessType, + parcelVolume: _parcelVolume, + activeContracts: int.tryParse(_contractsCtrl.text) ?? 12, + provider: providerName, + efficiency: efficiency, + status: ClientStatus.newClient, + lastUpdated: 'Just now', + surveySubmitted: true, + notes: _notesCtrl.text.trim(), + dataConsent: DataConsent.full, + phone: _phoneCtrl.text.trim(), + frequency: _frequency, + businessState: _businessCity?.state ?? '', + transitType: '', + transitFrom: _businessCity?.name ?? '', + transitTo: _destCities.map((c) => c.name).join(', '), + neighbourhood: _neighbourhoodCtrl.text.trim(), + logisticsSegment: _selectedSegments.join(', '), + pincode: _pincodeCtrl.text.trim(), + surveyLat: loc.lat, + surveyLng: loc.lng, + surveyAddress: loc.address, + surveyZone: loc.zone, + surveyPincode: loc.pincode, + )); + _transition(_SurveyState.success); + } + + void _transition(_SurveyState next) { + _animCtrl.reset(); + setState(() => _state = next); + _animCtrl.forward(); + } + + void _reset() { + _nameCtrl.clear(); + _phoneCtrl.clear(); + _neighbourhoodCtrl.clear(); + _pincodeCtrl.clear(); + _customProviderCtrl.clear(); + _contractsCtrl.text = '12'; + _notesCtrl.clear(); + setState(() { + _businessCity = null; + _destCities = []; + _capturedLocation = null; + _businessType = BusinessType.ecommerce; + _selectedSegments = ['First Mile']; + _frequency = ''; + _parcelVolume = 450; + _provider = 'DTDC Express'; + _step = 1; + _onConsentStep = false; + }); + _transition(_SurveyState.idle); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + FadeTransition( + opacity: _fadeAnim, + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.fromLTRB(16, 20, 16, 100), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WideTopBanner( + backendReady: widget.backendReady, + subtitle: 'SURVEY CONSOLE', + ), + const SizedBox(height: 20), + _buildBody(), + ], + ), + ), + ), + if (_capturingLocation) _buildLocationOverlay(), + ], + ); + } + + Widget _buildLocationOverlay() { + return Container( + color: Colors.black.withValues(alpha: 0.55), + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 48), + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 28), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), blurRadius: 24) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + shape: BoxShape.circle, + ), + child: Icon(Icons.location_searching, + size: 28, color: AppColors.primary), + ), + const SizedBox(height: 16), + const Text('Getting Location', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 6), + Text( + 'Capturing GPS for field visit log…', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, color: AppColors.textSecondary), + ), + const SizedBox(height: 18), + SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: + AlwaysStoppedAnimation(AppColors.primary), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildBody() { + switch (_state) { + case _SurveyState.idle: + return _buildIdle(); + case _SurveyState.surveying: + return _buildForm(); + case _SurveyState.success: + return _buildSuccess(); + } + } + + // ── Idle ────────────────────────────────────────────────────────────────── + + Widget _buildIdle() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.border.withValues(alpha: 0.25)), + ), + child: Column( + children: [ + Container( + width: 56, + height: 56, + decoration: const BoxDecoration( + color: Color(0xFFFFDAD7), shape: BoxShape.circle), + child: Icon(Icons.assignment_turned_in_outlined, + size: 28, color: AppColors.primary), + ), + const SizedBox(height: 14), + const Text('Welcome back', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue)), + const SizedBox(height: 4), + Text('Ready to start your next logistics survey?', + style: TextStyle( + fontSize: 13, color: AppColors.textSecondary), + textAlign: TextAlign.center), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton.icon( + onPressed: () => _transition(_SurveyState.surveying), + icon: const Icon(Icons.insert_drive_file_outlined, + size: 18, color: Colors.white), + label: const Text('Start New Survey', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 15)), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14)), + elevation: 2, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('LAST 3 SURVEYED CLIENTS', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.2, + color: AppColors.textSecondary)), + GestureDetector( + onTap: () => widget.onNavigate(1), + child: Text('View all', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppColors.primary)), + ), + ], + ), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.border.withValues(alpha: 0.15)), + ), + child: Column( + children: _lastSurveyed.isEmpty + ? [ + Padding( + padding: const EdgeInsets.all(20), + child: Text('No surveys yet', + style: + TextStyle(color: AppColors.textSecondary)), + ) + ] + : _lastSurveyed.asMap().entries.map((e) { + final isLast = e.key == _lastSurveyed.length - 1; + final client = e.value; + return InkWell( + onTap: () => widget.onNavigate(1), + borderRadius: isLast + ? const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16)) + : null, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 14), + decoration: BoxDecoration( + border: isLast + ? null + : Border( + bottom: BorderSide( + color: AppColors.border + .withValues(alpha: 0.1))), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFFEFF4FF), + shape: BoxShape.circle), + child: Icon(Icons.business, + size: 20, color: AppColors.primary), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Flexible( + child: Text(client.name, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + ), + if (client.dataConsent == + DataConsent.basicOnly) ...[ + const SizedBox(width: 6), + const ConsentBadge(small: true), + ], + ]), + Text( + '${client.city.isNotEmpty ? client.city : "—"} Ā· Submitted ${client.lastUpdated}', + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + ], + ), + ), + Icon(Icons.chevron_right, + size: 20, color: AppColors.muted), + ], + ), + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 20), + _SurveyBanner(), + ], + ); + } + + // ── Form wrapper ────────────────────────────────────────────────────────── + + Widget _buildForm() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.border.withValues(alpha: 0.3)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 10) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + if (_onConsentStep) { + setState(() => _onConsentStep = false); + } else if (_step > 1) { + setState(() => _step--); + } else { + _transition(_SurveyState.idle); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 6), + child: Row(children: [ + Icon(Icons.arrow_back_ios, + size: 14, color: AppColors.textSecondary), + const SizedBox(width: 4), + Text('Back', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.textSecondary)), + ]), + ), + ), + ), + Text( + _onConsentStep ? 'DATA SHARING' : 'STEP $_step OF 3', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 1.5, + color: _onConsentStep + ? const Color(0xFFE87C00) + : AppColors.primary), + ), + ], + ), + const SizedBox(height: 14), + + // Progress + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: _onConsentStep ? 1 / 3 : _step / 3, + minHeight: 6, + backgroundColor: const Color(0xFFF8F9FF), + valueColor: AlwaysStoppedAnimation( + _onConsentStep + ? const Color(0xFFFFB800) + : AppColors.primary, + ), + ), + ), + const SizedBox(height: 20), + + // Content + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: KeyedSubtree( + key: ValueKey(_onConsentStep ? 'consent' : 'step$_step'), + child: _onConsentStep + ? _buildConsentStep() + : _step == 1 + ? _buildStep1() + : _step == 2 + ? _buildStep2() + : _buildStep3(), + ), + ), + + // Nav buttons (hidden on consent step — cards act as buttons) + if (!_onConsentStep) ...[ + const SizedBox(height: 20), + Divider(height: 1, color: AppColors.border.withValues(alpha: 0.15)), + const SizedBox(height: 16), + Row(children: [ + if (_step > 1) + Expanded( + child: OutlinedButton.icon( + onPressed: () => setState(() => _step--), + icon: const Icon(Icons.arrow_back_ios, size: 14), + label: const Text('Back'), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.textSecondary, + side: BorderSide(color: AppColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + minimumSize: const Size.fromHeight(44), + ), + ), + ), + if (_step > 1) const SizedBox(width: 10), + Expanded( + child: _step < 3 + ? ElevatedButton.icon( + onPressed: () { + if (_step == 1) { + if (_nameCtrl.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text( + 'Please enter a Client Name first.'), + backgroundColor: AppColors.primary, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10)), + ), + ); + return; + } + setState(() => _onConsentStep = true); + } else { + setState(() => _step++); + } + }, + icon: const Icon(Icons.arrow_forward_ios, + size: 14, color: AppColors.darkBlue), + label: const Text('Continue', + style: TextStyle( + color: AppColors.darkBlue, + fontWeight: FontWeight.w700)), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD3E4FE), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + minimumSize: const Size.fromHeight(44), + elevation: 0, + ), + ) + : ElevatedButton.icon( + onPressed: () => _captureAndSubmit(basicOnly: false), + icon: const Icon(Icons.send, + size: 14, color: Colors.white), + label: const Text('Submit Survey', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700)), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + minimumSize: const Size.fromHeight(44), + elevation: 1, + ), + ), + ), + ]), + ], + ], + ), + ); + } + + // ── Step 1: client identity + contact + routing ─────────────────────────── + + Widget _buildStep1() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Basic Details', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 4), + Text("Client identity and transit preferences.", + style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), + const SizedBox(height: 18), + + // Company name + _fieldLabel('Client Name'), + const SizedBox(height: 6), + TextField( + controller: _nameCtrl, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + hintText: 'e.g. Murugan Transports', + prefixIcon: Icon(Icons.business, size: 18), + ), + ), + const SizedBox(height: 14), + + // Phone + _fieldLabel('Phone Number'), + const SizedBox(height: 6), + TextField( + controller: _phoneCtrl, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + hintText: '+91 98400-12345', + prefixIcon: Icon(Icons.phone_outlined, size: 18), + ), + ), + const SizedBox(height: 14), + + // Business Location — auto-detected from device GPS + _fieldLabel('Business Location'), + const SizedBox(height: 6), + _LocationDetectCard( + detecting: _detectingLocation || _capturingLocation, + city: _businessCity, + detectedAddress: _capturedLocation?.address ?? '', + onDetect: _detectLocation, + onPickManually: () => _openCityPicker( + title: 'Select Business City', + onPicked: (c) => setState(() => _businessCity = c), + ), + ), + const SizedBox(height: 14), + + // Neighbourhood + _fieldLabel('Neighbourhood'), + const SizedBox(height: 6), + TextField( + controller: _neighbourhoodCtrl, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + hintText: 'e.g. T. Nagar, Anna Nagar...', + prefixIcon: Icon(Icons.map_outlined, size: 18), + ), + ), + const SizedBox(height: 14), + + // Pincode — optional; auto-filled from GPS but editable. + _fieldLabel('PIN Code (optional)'), + const SizedBox(height: 6), + TextField( + controller: _pincodeCtrl, + keyboardType: TextInputType.number, + maxLength: 6, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration( + counterText: '', + hintText: 'e.g. 641001', + prefixIcon: Icon(Icons.local_post_office_outlined, size: 18), + ), + ), + const SizedBox(height: 14), + + // Shipping route — origin is the GPS-detected business location, + // destination is picked flight-booking style. + _fieldLabel('Primary Shipping Route'), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.border.withValues(alpha: 0.4)), + ), + child: Row(children: [ + Expanded( + child: _RouteEndpoint( + icon: Icons.flight_takeoff, + label: 'FROM', + code: _businessCity?.code ?? '—', + name: _businessCity?.name ?? 'Business location', + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Icon(Icons.arrow_forward, + size: 18, color: AppColors.primary), + ), + Expanded( + child: GestureDetector( + onTap: () => _openMultiCityPicker( + title: 'Select Destination Cities', + initialSelected: _destCities, + onPicked: (cities) => setState(() => _destCities = cities), + ), + child: _RouteEndpoint( + icon: Icons.flight_land, + label: 'TO', + code: _destCities.isEmpty + ? 'Tap' + : _destCities.length == 1 + ? _destCities.first.code + : '${_destCities.length} cities', + name: _destCities.isEmpty + ? 'Choose city' + : _destCities.map((c) => c.name).join(', '), + highlight: true, + ), + ), + ), + ]), + ), + const SizedBox(height: 14), + + // Frequency + _fieldLabel('Shipment Frequency'), + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 6, + children: _frequencyOptions.map((f) { + final selected = _frequency == f; + return GestureDetector( + onTap: () => setState(() => _frequency = f), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: selected ? AppColors.primary : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: selected + ? AppColors.primary + : AppColors.border.withValues(alpha: 0.6), + ), + ), + child: Text(f, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: selected + ? Colors.white + : AppColors.textSecondary, + )), + ), + ); + }).toList(), + ), + const SizedBox(height: 14), + + // Business type + _fieldLabel('Business Type'), + const SizedBox(height: 8), + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 3.5, + children: BusinessType.values.map((t) { + final selected = _businessType == t; + return GestureDetector( + onTap: () => setState(() => _businessType = t), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: selected + ? const Color(0xFFD3E4FE) + : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: selected + ? const Color(0xFFA6C8FF) + : AppColors.border.withValues(alpha: 0.5), + ), + ), + child: Center( + child: Text(t.label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: selected + ? AppColors.darkBlue + : AppColors.textSecondary, + )), + ), + ), + ); + }).toList(), + ), + + // Logistics Segment + const SizedBox(height: 14), + _fieldLabel('Logistics Segment'), + const SizedBox(height: 8), + Row( + children: ['First Mile', 'Middle Mile', 'Last Mile'].map((segment) { + final selected = _selectedSegments.contains(segment); + IconData iconData; + switch (segment) { + case 'First Mile': + iconData = Icons.warehouse_outlined; + break; + case 'Middle Mile': + iconData = Icons.local_shipping_outlined; + break; + default: + iconData = Icons.markunread_mailbox_outlined; + break; + } + return Expanded( + child: Padding( + padding: EdgeInsets.only( + left: segment == 'First Mile' ? 0 : 4, + right: segment == 'Last Mile' ? 0 : 4, + ), + child: GestureDetector( + onTap: () { + setState(() { + if (selected) { + _selectedSegments.remove(segment); + } else { + _selectedSegments.add(segment); + } + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: selected + ? const Color(0xFFEFF4FF) + : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selected + ? AppColors.primary + : AppColors.border.withValues(alpha: 0.5), + width: selected ? 1.5 : 1.0, + ), + boxShadow: selected + ? [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.08), + blurRadius: 8, + offset: const Offset(0, 4), + ) + ] + : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + iconData, + size: 20, + color: selected ? AppColors.primary : AppColors.textSecondary, + ), + const SizedBox(height: 6), + Text( + segment, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: selected + ? AppColors.darkBlue + : AppColors.textSecondary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + // Custom premium checkbox + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: selected ? AppColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: selected ? AppColors.primary : AppColors.border, + width: 1.5, + ), + ), + child: Center( + child: AnimatedScale( + duration: const Duration(milliseconds: 150), + scale: selected ? 1.0 : 0.0, + child: const Icon( + Icons.check, + size: 11, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ), + ], + ); + } + + // ── Step 2: logistics metrics (volume with peer avg hint) ──────────────── + + Widget _buildStep2() { + final avg = _avgVolumeForType; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Volume & Metrics', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 4), + Text('Estimate their weekly shipments.', + style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), + const SizedBox(height: 16), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _fieldLabel('Weekly Parcel Volume'), + Text('${_parcelVolume.round()} /wk', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + fontFamily: 'monospace', + color: AppColors.primary)), + ], + ), + const SizedBox(height: 8), + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: AppColors.primary, + inactiveTrackColor: const Color(0xFFF0F4FF), + thumbColor: AppColors.primary, + overlayColor: AppColors.primary.withValues(alpha: 0.1), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), + ), + child: Slider( + value: _parcelVolume, + min: 50, + max: 1200, + divisions: 115, + onChanged: (v) => setState(() => _parcelVolume = v), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('50 (Low)', + style: TextStyle( + fontSize: 10, color: AppColors.textSecondary)), + Text('1,200 (Extreme)', + style: TextStyle( + fontSize: 10, color: AppColors.textSecondary)), + ], + ), + + // peer average hint (computed from local client list) + if (avg > 0) ...[ + const SizedBox(height: 10), + GestureDetector( + onTap: () => + setState(() => _parcelVolume = avg.toDouble()), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.25)), + ), + child: Row(children: [ + Icon(Icons.insights, + size: 14, color: AppColors.primary), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Avg for ${_businessType.label} clients in your database: $avg /wk — tap to apply', + style: TextStyle( + fontSize: 11, color: AppColors.primary), + ), + ), + ]), + ), + ), + ], + + const SizedBox(height: 20), + _fieldLabel('Active Contracts'), + const SizedBox(height: 6), + TextField( + controller: _contractsCtrl, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.description_outlined, size: 18), + ), + ), + ], + ); + } + + // ── Step 3: provider & notes ────────────────────────────────────────────── + + Widget _buildStep3() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Provider & Notes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 4), + Text('Current logistics setup and extra details.', + style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), + const SizedBox(height: 16), + _fieldLabel('Current Provider'), + const SizedBox(height: 6), + InkWell( + onTap: _showProviderSelectorSheet, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.border.withValues(alpha: 0.5)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + _provider, + style: const TextStyle( + fontSize: 13, + color: AppColors.darkBlue, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Icon(Icons.arrow_drop_down, color: AppColors.textSecondary), + ], + ), + ), + ), + if (_provider == 'Others') ...[ + const SizedBox(height: 12), + _fieldLabel('Specify Other Provider'), + const SizedBox(height: 6), + TextField( + controller: _customProviderCtrl, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration( + hintText: 'e.g. My Local Logistics Operator', + prefixIcon: Icon(Icons.edit_note, size: 18), + ), + ), + ], + const SizedBox(height: 16), + _fieldLabel('Additional Notes'), + const SizedBox(height: 6), + TextField( + controller: _notesCtrl, + maxLines: 4, + decoration: const InputDecoration( + hintText: 'e.g. Planning to expand to Tamil Nadu soon...', + ), + ), + ], + ); + } + + // ── Consent step ────────────────────────────────────────────────────────── + + Widget _buildConsentStep() { + final clientName = _nameCtrl.text.trim(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Data Sharing Preference', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 4), + Text( + 'Is ${clientName.isEmpty ? "the client" : clientName} comfortable sharing full logistics details?', + style: TextStyle(fontSize: 12, color: AppColors.textSecondary), + ), + const SizedBox(height: 20), + + // Basic only + GestureDetector( + onTap: () => _captureAndSubmit(basicOnly: true), + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: const Color(0xFFFFF8ED), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFFFB800).withValues(alpha: 0.5)), + ), + child: Row(children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFFFEDC2), + borderRadius: BorderRadius.circular(12)), + child: const Icon(Icons.lock_outline, + size: 22, color: Color(0xFFE87C00)), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Basic Info Only', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 2), + Text( + 'Save identity & routing only. Commercial details can be added later.', + style: TextStyle( + fontSize: 11, color: AppColors.textSecondary), + ), + ], + ), + ), + Icon(Icons.chevron_right, size: 18, color: AppColors.muted), + ]), + ), + ), + const SizedBox(height: 12), + + // Full details + GestureDetector( + onTap: () => setState(() { + _onConsentStep = false; + _step = 2; + }), + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.35)), + ), + child: Row(children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFD3E4FE), + borderRadius: BorderRadius.circular(12)), + child: Icon(Icons.lock_open_outlined, + size: 22, color: AppColors.primary), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Share Full Details', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + const SizedBox(height: 2), + Text('Include volume, contracts, provider & notes.', + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + ], + ), + ), + Icon(Icons.chevron_right, + size: 18, color: AppColors.primary), + ]), + ), + ), + const SizedBox(height: 16), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.border.withValues(alpha: 0.2)), + ), + child: Row(children: [ + Icon(Icons.info_outline, + size: 14, color: AppColors.textSecondary), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Basic-only clients can be upgraded to full profile at any time from the Client Database.', + style: TextStyle( + fontSize: 10, color: AppColors.textSecondary), + ), + ), + ]), + ), + ], + ); + } + + // ── Success ─────────────────────────────────────────────────────────────── + + Widget _buildSuccess() { + final isBasic = _chosenConsent == DataConsent.basicOnly; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.border.withValues(alpha: 0.2)), + ), + child: Column( + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: isBasic + ? const Color(0xFFFFF8ED) + : const Color(0xFFECFDF5), + shape: BoxShape.circle, + ), + child: Icon( + isBasic ? Icons.lock_outline : Icons.check_circle, + size: 40, + color: isBasic + ? const Color(0xFFE87C00) + : const Color(0xFF10B981), + ), + ), + const SizedBox(height: 20), + Text( + isBasic ? 'Basic Info Saved!' : 'Survey Submitted!', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue), + ), + const SizedBox(height: 8), + Text( + isBasic + ? 'Client recorded with identity & routing. Collect commercial details later from the Client Database.' + : 'Full profile saved and metrics recalculated.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13, color: AppColors.textSecondary), + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8F9FF), + borderRadius: BorderRadius.circular(14), + border: + Border.all(color: AppColors.border.withValues(alpha: 0.1)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('ENTRY SUMMARY', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: isBasic + ? const Color(0xFFFFEDC2) + : const Color(0xFFD8F9EE), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isBasic ? 'BASIC ONLY' : 'COMPLETE', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + color: isBasic + ? const Color(0xFFE87C00) + : AppColors.green), + ), + ), + ], + ), + Divider( + height: 14, + color: AppColors.border.withValues(alpha: 0.15)), + _summaryRow('Client Name', _nameCtrl.text), + if (_phoneCtrl.text.isNotEmpty) + _summaryRow('Phone', _phoneCtrl.text), + if (_pincodeCtrl.text.isNotEmpty) + _summaryRow('PIN Code', _pincodeCtrl.text), + _summaryRow( + 'Location', + [ + if (_businessCity != null) _businessCity!.name, + if (_businessCity != null) _businessCity!.state, + if (_neighbourhoodCtrl.text.isNotEmpty) + _neighbourhoodCtrl.text, + ].join(', ').ifEmpty('—')), + if (_businessCity != null || _destCities.isNotEmpty) + _summaryRow('Route', + '${_businessCity?.code ?? "—"} → ${_destCities.isEmpty ? "—" : _destCities.map((c) => c.code).join(", ")}'), + if (_frequency.isNotEmpty) + _summaryRow('Frequency', _frequency), + if (_selectedSegments.isNotEmpty) + _summaryRow('Segment', _selectedSegments.join(', ')), + if (!isBasic) ...[ + _summaryRow( + 'Volume', '${_parcelVolume.round()} /wk'), + _summaryRow('Provider', _provider), + ], + ], + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton( + onPressed: () { widget.onNavigate(0); _reset(); }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Go to Dashboard', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + height: 44, + child: OutlinedButton( + onPressed: _reset, + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: Text('Log Another Survey', + style: TextStyle( + color: AppColors.textSecondary, + fontWeight: FontWeight.w600)), + ), + ), + ], + ), + ); + } + + Widget _fieldLabel(String text) => Text(text, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)); + + /// Opens the searchable city picker bottom sheet. + Future _openCityPicker({ + required String title, + required void Function(IndiaCity) onPicked, + }) async { + final picked = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _CityPickerSheet(title: title), + ); + if (picked != null) onPicked(picked); + } + + // ── Searchable multi-city picker bottom sheet ──────────────────────────────── + Future _openMultiCityPicker({ + required String title, + required List initialSelected, + required void Function(List) onPicked, + }) async { + final picked = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _MultiCityPickerSheet( + title: title, + initialSelected: initialSelected, + ), + ); + if (picked != null) { + onPicked(picked); + } + } + + Future _showProviderSelectorSheet() async { + final picked = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ProviderPickerSheet( + initialProvider: _provider, + ), + ); + if (picked != null) { + setState(() { + _provider = picked; + }); + } + } +} + +// ── GPS business-location detect card ──────────────────────────────────────── + +class _LocationDetectCard extends StatelessWidget { + final bool detecting; + final IndiaCity? city; + final String detectedAddress; + final VoidCallback onDetect; + final VoidCallback onPickManually; + + const _LocationDetectCard({ + required this.detecting, + required this.city, + required this.detectedAddress, + required this.onDetect, + required this.onPickManually, + }); + + @override + Widget build(BuildContext context) { + final hasCity = city != null; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: hasCity ? const Color(0xFFEFF4FF) : const Color(0xFFF8F9FF), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: hasCity + ? AppColors.primary.withValues(alpha: 0.4) + : AppColors.border.withValues(alpha: 0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: hasCity + ? AppColors.primary + : AppColors.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + hasCity ? Icons.location_on : Icons.my_location, + size: 20, + color: hasCity ? Colors.white : AppColors.primary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: hasCity + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Flexible( + child: Text(city!.name, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue), + overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(4), + ), + child: Text(city!.code, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w800, + color: Colors.white)), + ), + ]), + if (city!.state.isNotEmpty) + Text(city!.state, + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Detect from GPS', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + Text('Auto-fill the client\'s business city', + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + ], + ), + ), + if (detecting) + const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2.5), + ) + else + IconButton( + onPressed: onDetect, + icon: Icon(Icons.gps_fixed, color: AppColors.primary), + tooltip: 'Detect via GPS', + ), + ], + ), + if (hasCity && detectedAddress.isNotEmpty) ...[ + const SizedBox(height: 8), + Text(detectedAddress, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.5, color: AppColors.textSecondary)), + ], + const SizedBox(height: 10), + Row(children: [ + if (!detecting) + Expanded( + child: OutlinedButton.icon( + onPressed: onDetect, + icon: const Icon(Icons.my_location, size: 14), + label: Text(hasCity ? 'Re-detect' : 'Use GPS Location', + style: const TextStyle( + fontSize: 12, fontWeight: FontWeight.w700)), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primary, + side: BorderSide(color: AppColors.primary.withValues(alpha: 0.5)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + minimumSize: const Size.fromHeight(38), + ), + ), + ), + if (!detecting) const SizedBox(width: 8), + Expanded( + child: TextButton.icon( + onPressed: onPickManually, + icon: const Icon(Icons.search, size: 14), + label: const Text('Choose City', + style: TextStyle( + fontSize: 12, fontWeight: FontWeight.w700)), + style: TextButton.styleFrom( + foregroundColor: AppColors.textSecondary, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: AppColors.border.withValues(alpha: 0.5)), + ), + minimumSize: const Size.fromHeight(38), + ), + ), + ), + ]), + ], + ), + ); + } +} + +// ── Flight-style route endpoint ─────────────────────────────────────────────── + +class _RouteEndpoint extends StatelessWidget { + final IconData icon; + final String label; + final String code; + final String name; + final bool highlight; + + const _RouteEndpoint({ + required this.icon, + required this.label, + required this.code, + required this.name, + this.highlight = false, + }); + + @override + Widget build(BuildContext context) { + final color = highlight ? AppColors.primary : AppColors.darkBlue; + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(icon, size: 11, color: AppColors.textSecondary), + const SizedBox(width: 3), + Text(label, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + color: AppColors.textSecondary)), + ]), + const SizedBox(height: 3), + Text(code, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: color), + maxLines: 1, + overflow: TextOverflow.ellipsis), + Text(name, + style: TextStyle( + fontSize: 10, + color: AppColors.textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis), + ], + ); + } +} + +// ── Searchable city picker bottom sheet ─────────────────────────────────────── + +class _CityPickerSheet extends StatefulWidget { + final String title; + const _CityPickerSheet({required this.title}); + + @override + State<_CityPickerSheet> createState() => _CityPickerSheetState(); +} + +class _CityPickerSheetState extends State<_CityPickerSheet> { + final _searchCtrl = TextEditingController(); + String _query = ''; + + @override + void dispose() { + _searchCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final results = + indiaCities.where((c) => c.matches(_query)).toList(); + + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (_, scrollCtrl) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.muted.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), + child: Row(children: [ + Expanded( + child: Text(widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue)), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFF8F9FF), shape: BoxShape.circle), + child: Icon(Icons.close, + size: 16, color: AppColors.textSecondary), + ), + ), + ]), + ), + // Search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextField( + controller: _searchCtrl, + autofocus: true, + onChanged: (v) => setState(() => _query = v), + decoration: InputDecoration( + hintText: 'Search city, code or state…', + hintStyle: + TextStyle(color: AppColors.muted, fontSize: 13), + prefixIcon: + Icon(Icons.search, color: AppColors.muted, size: 20), + suffixIcon: _query.isEmpty + ? null + : IconButton( + icon: Icon(Icons.clear, + size: 18, color: AppColors.muted), + onPressed: () { + _searchCtrl.clear(); + setState(() => _query = ''); + }, + ), + ), + ), + ), + const SizedBox(height: 8), + Expanded( + child: results.isEmpty + ? Center( + child: Text('No cities match "$_query"', + style: + TextStyle(color: AppColors.textSecondary)), + ) + : ListView.builder( + controller: scrollCtrl, + padding: const EdgeInsets.symmetric(horizontal: 12), + itemCount: results.length, + itemBuilder: (_, i) { + final c = results[i]; + return ListTile( + onTap: () => Navigator.pop(context, c), + leading: Container( + width: 46, + height: 34, + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text(c.code, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: AppColors.primary)), + ), + title: Text(c.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + subtitle: Text(c.state, + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + trailing: Icon(Icons.chevron_right, + size: 18, color: AppColors.muted), + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +// ── Survey banner (enhanced) ────────────────────────────────────────────────── + +class _SurveyBanner extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + height: 150, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0B1C30), // Dark Blue + Color(0xFF450A0A), // Dark Red/Burgundy + ], + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF0B1C30).withValues(alpha: 0.15), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Stack( + children: [ + // Glow orb 1 (Red Accent) + Positioned( + right: -30, + top: -30, + child: Container( + width: 140, + height: 140, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.primary.withValues(alpha: 0.25), + ), + ), + ), + // Glow orb 2 (Blue Accent) + Positioned( + left: 80, + bottom: -40, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.blueAccent.withValues(alpha: 0.15), + ), + ), + ), + // Dot Grid overlay + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: CustomPaint(painter: _DotGridPainter()), + ), + ), + // Main content + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Tag/Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Colors.white.withValues(alpha: 0.15), + ), + ), + child: const Text( + 'SMART CRM CORE', + style: TextStyle( + fontSize: 8, + fontWeight: FontWeight.w800, + color: Color(0xFFFFF0F1), + letterSpacing: 1.0, + ), + ), + ), + const SizedBox(height: 10), + const Text( + 'Streamlined Surveys', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w800, + color: Colors.white, + letterSpacing: 0.4, + ), + ), + const SizedBox(height: 4), + Text( + 'Capturing field data has never been easier. Syncs automatically.', + style: TextStyle( + fontSize: 11, + color: Colors.white.withValues(alpha: 0.85), + height: 1.3, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + // Glassmorphic circular icon container + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1.5, + ), + ), + child: const Center( + child: Icon( + Icons.rocket_launch_rounded, + size: 24, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +// ── Consent badge (reused by clients_screen) ────────────────────────────────── + +class ConsentBadge extends StatelessWidget { + final bool small; + const ConsentBadge({super.key, this.small = false}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric( + horizontal: small ? 5 : 7, vertical: small ? 2 : 3), + decoration: BoxDecoration( + color: const Color(0xFFFFEDC2), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.lock_outline, + size: small ? 8 : 10, color: const Color(0xFFE87C00)), + SizedBox(width: small ? 2 : 3), + Text('BASIC', + style: TextStyle( + fontSize: small ? 8 : 9, + fontWeight: FontWeight.w800, + color: const Color(0xFFE87C00))), + ], + ), + ); + } +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +Widget _summaryRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: TextStyle( + fontSize: 11, color: AppColors.textSecondary)), + Flexible( + child: Text(value, + textAlign: TextAlign.end, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: AppColors.darkBlue)), + ), + ], + ), + ); +} + +extension _StringX on String { + String ifEmpty(String fallback) => isEmpty ? fallback : this; +} + +// ── Dot grid painter ────────────────────────────────────────────────────────── + +class _DotGridPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withValues(alpha: 0.06) + ..strokeWidth = 1; + const spacing = 28.0; + for (double x = 0; x < size.width; x += spacing) { + for (double y = 0; y < size.height; y += spacing) { + canvas.drawCircle(Offset(x, y), 1.5, paint); + } + } + } + + @override + bool shouldRepaint(_DotGridPainter oldDelegate) => false; +} + + + +class _MultiCityPickerSheet extends StatefulWidget { + final String title; + final List initialSelected; + + const _MultiCityPickerSheet({ + required this.title, + required this.initialSelected, + }); + + @override + State<_MultiCityPickerSheet> createState() => _MultiCityPickerSheetState(); +} + +class _MultiCityPickerSheetState extends State<_MultiCityPickerSheet> { + final _searchCtrl = TextEditingController(); + String _query = ''; + late List _selectedCities; + + @override + void initState() { + super.initState(); + _selectedCities = List.from(widget.initialSelected); + } + + @override + void dispose() { + _searchCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final results = indiaCities.where((c) => c.matches(_query)).toList(); + + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (_, scrollCtrl) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.muted.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), + child: Row( + children: [ + Expanded( + child: Text(widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue)), + ), + if (_selectedCities.isNotEmpty) + TextButton( + onPressed: () => setState(() => _selectedCities.clear()), + style: TextButton.styleFrom( + foregroundColor: AppColors.primary, + padding: const EdgeInsets.symmetric(horizontal: 8), + ), + child: const Text('Clear All', + style: TextStyle( + fontSize: 13, fontWeight: FontWeight.bold)), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFF8F9FF), shape: BoxShape.circle), + child: Icon(Icons.close, + size: 16, color: AppColors.textSecondary), + ), + ), + ], + ), + ), + // Search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextField( + controller: _searchCtrl, + autofocus: true, + onChanged: (v) => setState(() => _query = v), + decoration: InputDecoration( + hintText: 'Search city, code or state…', + hintStyle: + TextStyle(color: AppColors.muted, fontSize: 13), + prefixIcon: + Icon(Icons.search, color: AppColors.muted, size: 20), + suffixIcon: _query.isEmpty + ? null + : IconButton( + icon: Icon(Icons.clear, + size: 18, color: AppColors.muted), + onPressed: () { + _searchCtrl.clear(); + setState(() => _query = ''); + }, + ), + ), + ), + ), + const SizedBox(height: 8), + Expanded( + child: results.isEmpty + ? Center( + child: Text('No cities match "$_query"', + style: + TextStyle(color: AppColors.textSecondary)), + ) + : ListView.builder( + controller: scrollCtrl, + padding: const EdgeInsets.symmetric(horizontal: 12), + itemCount: results.length, + itemBuilder: (_, i) { + final c = results[i]; + final isSelected = _selectedCities.any((x) => x.name == c.name); + return CheckboxListTile( + value: isSelected, + onChanged: (val) { + setState(() { + if (val == true) { + if (!_selectedCities.any((x) => x.name == c.name)) { + _selectedCities.add(c); + } + } else { + _selectedCities.removeWhere((x) => x.name == c.name); + } + }); + }, + activeColor: AppColors.primary, + checkboxShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + title: Text(c.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue)), + subtitle: Text(c.state, + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary)), + secondary: Container( + width: 46, + height: 34, + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text(c.code, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: AppColors.primary)), + ), + ); + }, + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: () => Navigator.pop(context, _selectedCities), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 1, + ), + child: Text( + _selectedCities.isEmpty + ? 'Apply' + : 'Apply (${_selectedCities.length} Selected)', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w700), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class ProviderPickerSheet extends StatefulWidget { + final String initialProvider; + + const ProviderPickerSheet({ + super.key, + required this.initialProvider, + }); + + @override + State createState() => _ProviderPickerSheetState(); +} + +class _ProviderPickerSheetState extends State { + final _searchCtrl = TextEditingController(); + String _query = ''; + + @override + void dispose() { + _searchCtrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final lowercaseQuery = _query.toLowerCase(); + + // Build grouped results based on the search query + final Map> filteredGroups = {}; + + categorisedProviders.forEach((category, providers) { + final matches = providers.where((p) => p.toLowerCase().contains(lowercaseQuery)).toList(); + if (matches.isNotEmpty) { + filteredGroups[category] = matches; + } + }); + + final showOthers = _query.isEmpty || 'others'.contains(lowercaseQuery); + + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (_, scrollCtrl) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 10), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.muted.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), + child: Row( + children: [ + const Expanded( + child: Text('Select Service Provider', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue)), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: Color(0xFFF8F9FF), shape: BoxShape.circle), + child: Icon(Icons.close, + size: 16, color: AppColors.textSecondary), + ), + ), + ], + ), + ), + // Search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextField( + controller: _searchCtrl, + autofocus: true, + onChanged: (v) => setState(() => _query = v), + decoration: InputDecoration( + hintText: 'Search service provider...', + hintStyle: + TextStyle(color: AppColors.muted, fontSize: 13), + prefixIcon: + Icon(Icons.search, color: AppColors.muted, size: 20), + suffixIcon: _query.isEmpty + ? null + : IconButton( + icon: Icon(Icons.clear, + size: 18, color: AppColors.muted), + onPressed: () { + _searchCtrl.clear(); + setState(() => _query = ''); + }, + ), + ), + ), + ), + const SizedBox(height: 8), + Expanded( + child: (filteredGroups.isEmpty && !showOthers) + ? Center( + child: Text('No providers match "$_query"', + style: + TextStyle(color: AppColors.textSecondary)), + ) + : ListView.builder( + controller: scrollCtrl, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: (showOthers ? 1 : 0) + filteredGroups.length * 2, // category title + items + itemBuilder: (context, index) { + if (showOthers && index == 0) { + // Render "Others" option + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ListTile( + onTap: () => Navigator.pop(context, 'Others'), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + leading: Container( + width: 36, + height: 36, + decoration: const BoxDecoration( + color: Color(0xFFFFF8ED), + shape: BoxShape.circle, + ), + child: const Icon(Icons.add, color: Color(0xFFE87C00), size: 18), + ), + title: const Text( + 'Others', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue, + ), + ), + subtitle: const Text( + 'Manually enter custom provider', + style: TextStyle( + fontSize: 11, + color: Color(0xFFE87C00), + fontWeight: FontWeight.w500, + ), + ), + trailing: widget.initialProvider == 'Others' + ? const Icon(Icons.check_circle, color: AppColors.primary, size: 20) + : null, + ), + ); + } + + // Calculate the actual index in the groups + int groupIndex = showOthers ? index - 1 : index; + int mapIndex = groupIndex ~/ 2; + bool isHeader = groupIndex % 2 == 0; + + final keys = filteredGroups.keys.toList(); + final category = keys[mapIndex]; + final providers = filteredGroups[category]!; + + if (isHeader) { + return Padding( + padding: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 12.0), + child: Text( + category.toUpperCase(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + letterSpacing: 1.0, + color: AppColors.primary, + ), + ), + ); + } else { + return Column( + children: providers.map((provider) { + final isSelected = widget.initialProvider == provider; + return ListTile( + onTap: () => Navigator.pop(context, provider), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + title: Text( + provider, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: AppColors.darkBlue, + ), + ), + trailing: isSelected + ? const Icon(Icons.check_circle, color: AppColors.primary, size: 20) + : null, + ); + }).toList(), + ); + } + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/surveys_list_screen.dart b/lib/screens/surveys_list_screen.dart new file mode 100644 index 0000000..cfbe96f --- /dev/null +++ b/lib/screens/surveys_list_screen.dart @@ -0,0 +1,843 @@ +import 'package:flutter/material.dart'; +import '../services/crm_api_service.dart'; +import '../theme/app_theme.dart'; +import '../utils/snackbar_utils.dart'; + +class SurveysListScreen extends StatefulWidget { + final List surveys; + final VoidCallback onDataChanged; + final bool isActive; + + const SurveysListScreen({ + super.key, + required this.surveys, + required this.onDataChanged, + this.isActive = false, + }); + + @override + State createState() => _SurveysListScreenState(); +} + +class _SurveysListScreenState extends State { + final CrmApiService _api = CrmApiService(); + bool _isSaving = false; + + void _deleteSurvey(String id) async { + final confirm = await showDialog( + context: context, + builder: (ctx) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: AppColors.primary.withValues(alpha: 0.1), shape: BoxShape.circle), + child: const Icon(Icons.warning_amber_rounded, color: AppColors.primary, size: 32), + ), + const SizedBox(height: 16), + const Text('Delete Survey?', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), + const SizedBox(height: 8), + const Text('This action cannot be undone. Are you sure you want to delete this record?', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: AppColors.textSecondary)), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(ctx, false), + style: TextButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), + child: const Text('Cancel', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Delete', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), + ), + ), + ], + ), + ], + ), + ), + ), + ); + + if (confirm != true) return; + + setState(() => _isSaving = true); + final success = await _api.deleteSurvey(id); + setState(() => _isSaving = false); + + if (success) { + widget.onDataChanged(); + if (mounted) { + SnackbarUtils.showSuccess(context, 'Survey deleted successfully.'); + Navigator.pop(context); // Optional: close screen or refresh + } + } else { + if (mounted) { + SnackbarUtils.showError(context, 'Failed to delete survey.'); + } + } + } + + void _showEditSurveyDialog(Map survey) { + _showSurveyFormDialog(survey: survey); + } + + void _showAddSurveyDialog() { + _showSurveyFormDialog(); + } + + void _showSurveyFormDialog({Map? survey}) { + final isEditing = survey != null; + final companyCtrl = TextEditingController(text: survey?['company']?.toString() ?? ''); + final areaCtrl = TextEditingController(text: survey?['area']?.toString() ?? ''); + final phoneCtrl = TextEditingController(text: survey?['phone']?.toString() ?? ''); + final rateCtrl = TextEditingController(text: survey?['rate_per_kg']?.toString() ?? ''); + final addressCtrl = TextEditingController(text: survey?['address']?.toString() ?? ''); + final id = survey?['id']?.toString() ?? ''; + + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 20), + decoration: const BoxDecoration( + color: AppColors.blueLight, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Icon(isEditing ? Icons.edit_outlined : Icons.add_circle_outline, color: AppColors.primary, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(isEditing ? 'Edit Survey' : 'Add Competitor Survey', style: const TextStyle(fontWeight: FontWeight.w800, color: AppColors.darkBlue, fontSize: 18)), + const SizedBox(height: 2), + Text(isEditing ? 'Update competitor details' : 'Log a new provider survey', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + ), + // Form Fields + Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + _buildModernTextField( + controller: companyCtrl, + label: 'Company / Provider', + hint: 'e.g. DTDC, Delhivery', + icon: Icons.storefront_outlined, + ), + const SizedBox(height: 16), + _buildModernTextField( + controller: areaCtrl, + label: 'Area / Zone', + hint: 'e.g. SOUTH ZONE', + icon: Icons.place_outlined, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildModernTextField( + controller: phoneCtrl, + label: 'Contact Number', + hint: 'Optional', + icon: Icons.phone_outlined, + keyboardType: TextInputType.phone, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildModernTextField( + controller: rateCtrl, + label: 'Rate / KG', + hint: '0.00', + icon: Icons.currency_rupee, + keyboardType: TextInputType.number, + ), + ), + ], + ), + const SizedBox(height: 16), + _buildModernTextField( + controller: addressCtrl, + label: 'Full Address', + hint: 'Optional physical address', + icon: Icons.map_outlined, + maxLines: 2, + ), + ], + ), + ), + // Actions + Container( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(ctx), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text('Cancel', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + if (companyCtrl.text.isEmpty || areaCtrl.text.isEmpty) { + SnackbarUtils.showError(ctx, 'Company and Area are required'); + return; + } + + setState(() => _isSaving = true); + Navigator.pop(ctx); + + final payload = { + 'company': companyCtrl.text, + 'area': areaCtrl.text, + 'phone': phoneCtrl.text, + 'rate_per_kg': rateCtrl.text, + 'address': addressCtrl.text, + 'offers_pickup': survey?['offers_pickup'] ?? 'no', + 'offers_drop': survey?['offers_drop'] ?? 'no', + 'packing_charge': survey?['packing_charge'] ?? '', + 'time_in_days': survey?['time_in_days'] ?? '', + 'plus_code': survey?['plus_code'] ?? '', + 'frequency': survey?['frequency'] ?? 'Daily', + 'pincodes': survey?['pincodes'] ?? '', + }; + + final success = isEditing + ? await _api.updateSurvey(id, payload) + : await _api.createSurvey(payload); + + setState(() => _isSaving = false); + + if (success) { + widget.onDataChanged(); + if (mounted) { + SnackbarUtils.showSuccess(context, isEditing ? 'Survey updated!' : 'Survey added!'); + } + } else { + if (mounted) { + SnackbarUtils.showError(context, isEditing ? 'Update failed.' : 'Failed to add survey.'); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text(isEditing ? 'Save Changes' : 'Add Survey', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildModernTextField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + TextInputType keyboardType = TextInputType.text, + int maxLines = 1, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: AppColors.darkBlue, letterSpacing: 0.5), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + keyboardType: keyboardType, + maxLines: maxLines, + style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.darkBlue, fontSize: 14), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: AppColors.textSecondary.withValues(alpha: 0.5)), + prefixIcon: maxLines == 1 ? Icon(icon, size: 18, color: AppColors.textSecondary) : null, + filled: true, + fillColor: const Color(0xFFF8F9FA), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: maxLines == 1 ? 16 : 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade200), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary), + ), + ), + ), + ], + ); + } + + Widget _buildStatCard(String label, String value, IconData icon) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.02), blurRadius: 4, offset: const Offset(0, 2)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 16, color: AppColors.primary), + const SizedBox(height: 8), + Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), + const SizedBox(height: 2), + Text(label, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondary)), + ], + ), + ); + } + + // Group surveys by company + Map> get groupedSurveys { + final Map> map = {}; + for (var s in widget.surveys) { + final company = s['company']?.toString().trim(); + final key = (company == null || company.isEmpty) ? 'Unknown' : company; + if (!map.containsKey(key)) { + map[key] = []; + } + map[key]!.add(s); + } + return map; + } + + @override + Widget build(BuildContext context) { + final groups = groupedSurveys; + final companies = groups.keys.toList()..sort(); + + return Scaffold( + backgroundColor: AppColors.scaffold, + appBar: AppBar( + title: const Text('Competitor Intelligence', style: TextStyle(color: AppColors.darkBlue, fontWeight: FontWeight.w800)), + backgroundColor: Colors.white, + iconTheme: const IconThemeData(color: AppColors.darkBlue), + elevation: 0, + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _isSaving ? null : _showAddSurveyDialog, + backgroundColor: AppColors.primary, + icon: const Icon(Icons.add, color: Colors.white), + label: const Text('Add Provider', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700)), + ), + body: _isSaving + ? const Center(child: CircularProgressIndicator()) + : widget.surveys.isEmpty + ? const Center(child: Text('No surveys available.', style: TextStyle(color: AppColors.textSecondary))) + : CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.blueLight, Colors.white], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.primary.withValues(alpha: 0.1)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.radar_outlined, color: AppColors.primary, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Market Overview', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18, color: AppColors.darkBlue)), + const SizedBox(height: 2), + Text('Tracking ${widget.surveys.length} surveys across ${companies.length} providers', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildStatCard('Active Providers', companies.length.toString(), Icons.storefront), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard('Total Branches', widget.surveys.length.toString(), Icons.location_on_outlined), + ), + ], + ), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final company = companies[index]; + final locations = groups[company] ?? []; + final initial = company.isNotEmpty ? company.substring(0, 1).toUpperCase() : 'U'; + + return Card( + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: Colors.grey.shade200, width: 1), + ), + elevation: 0, + color: Colors.white, + child: ExpansionTile( + shape: const Border(), + tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: Text( + initial, + style: const TextStyle(color: AppColors.primary, fontSize: 20, fontWeight: FontWeight.w800), + ), + ), + title: Text( + company, + style: const TextStyle(fontWeight: FontWeight.w800, color: AppColors.darkBlue, fontSize: 16), + ), + subtitle: Row( + children: [ + const Icon(Icons.storefront, size: 14, color: AppColors.textSecondary), + const SizedBox(width: 4), + Text('${locations.length} Locations Surveyed', style: const TextStyle(color: AppColors.textSecondary, fontSize: 13)), + ], + ), + children: [ + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border(top: BorderSide(color: Colors.grey.shade200)), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), + ), + padding: const EdgeInsets.all(16), + child: Column( + children: locations.map((survey) { + return _SurveyLocationCard( + survey: survey as Map, + onEdit: () => _showEditSurveyDialog(survey), + onDelete: () => _deleteSurvey(survey['id'].toString()), + ); + }).toList(), + ), + ), + ], + ), + ); + }, + childCount: companies.length, + ), + ), + ), + const SliverToBoxAdapter(child: SizedBox(height: 80)), + ], + ), + ); + } +} + +class _SurveyLocationCard extends StatefulWidget { + final Map survey; + final VoidCallback onEdit; + final VoidCallback onDelete; + + const _SurveyLocationCard({ + required this.survey, + required this.onEdit, + required this.onDelete, + }); + + @override + State<_SurveyLocationCard> createState() => _SurveyLocationCardState(); +} + +class _SurveyLocationCardState extends State<_SurveyLocationCard> { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + final survey = widget.survey; + final address = survey['address']?.toString() ?? survey['area']?.toString() ?? 'No address'; + final area = survey['area']?.toString() ?? 'Unknown'; + final phone = survey['phone']?.toString() ?? 'N/A'; + final rate = survey['rate_per_kg']?.toString() ?? ''; + final hasRate = rate.isNotEmpty; + + final pickup = survey['offers_pickup']?.toString().toLowerCase() == 'yes' ? 'Yes' : '—'; + final drop = survey['offers_drop']?.toString().toLowerCase() == 'yes' ? 'Yes' : '—'; + final packing = survey['packing_charge']?.toString() ?? '—'; + final time = survey['time_in_days']?.toString() ?? '—'; + final freq = survey['frequency']?.toString() ?? '—'; + final plusCode = survey['plus_code']?.toString() ?? '—'; + final pincodes = survey['pincodes']?.toString() ?? '—'; + final company = survey['company']?.toString() ?? '—'; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + // Header Row + InkWell( + onTap: () => setState(() => _isExpanded = !_isExpanded), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.place_outlined, color: AppColors.primary, size: 20), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + area, + style: const TextStyle(fontWeight: FontWeight.w700, color: AppColors.darkBlue, fontSize: 14), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Row( + children: [ + const Icon(Icons.phone, size: 12, color: AppColors.textSecondary), + const SizedBox(width: 4), + Text(phone, style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)), + ], + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text('RATE PER KG', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: AppColors.textSecondary)), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: hasRate ? AppColors.green.withValues(alpha: 0.1) : Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + hasRate ? '₹$rate' : 'Not Answered', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: hasRate ? AppColors.greenDark : AppColors.textSecondary + ), + ), + ), + ], + ), + ], + ), + ), + ), + + if (!_isExpanded) + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), + child: Column( + children: [ + const Divider(height: 1), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('LOGISTICS', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: AppColors.textSecondary)), + Row( + children: [ + TextButton( + onPressed: () => setState(() => _isExpanded = true), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + minimumSize: const Size(0, 30), + ), + child: const Text('More Info', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), + ), + const SizedBox(width: 8), + InkWell( + onTap: widget.onEdit, + child: const Padding(padding: EdgeInsets.all(4.0), child: Icon(Icons.edit_outlined, size: 18, color: AppColors.textSecondary)), + ), + const SizedBox(width: 4), + InkWell( + onTap: widget.onDelete, + child: const Padding(padding: EdgeInsets.all(4.0), child: Icon(Icons.delete_outline, size: 18, color: AppColors.primaryDark)), + ), + ], + ), + ], + ), + ], + ), + ), + + if (_isExpanded) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)), + border: Border(top: BorderSide(color: Colors.grey.shade200)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => setState(() => _isExpanded = false), + style: TextButton.styleFrom( + backgroundColor: AppColors.primary.withValues(alpha: 0.05), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + minimumSize: const Size(0, 30), + ), + icon: const Text('Hide Info', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.primary)), + label: const Icon(Icons.expand_less, size: 16, color: AppColors.primary), + ), + const SizedBox(width: 12), + InkWell( + onTap: widget.onEdit, + child: const Padding(padding: EdgeInsets.all(4.0), child: Icon(Icons.edit_outlined, size: 18, color: AppColors.textSecondary)), + ), + const SizedBox(width: 4), + InkWell( + onTap: widget.onDelete, + child: const Padding(padding: EdgeInsets.all(4.0), child: Icon(Icons.delete_outline, size: 18, color: AppColors.primaryDark)), + ), + ], + ), + const SizedBox(height: 12), + // Grid of Details + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildDetailSection( + icon: Icons.assignment_outlined, + title: 'ENQUIRY DETAILS', + items: [ + _buildInfoItem('RATE PER KG', hasRate ? '₹$rate' : 'Not Answered', isHighlight: hasRate), + _buildInfoItem('PICKUP', pickup), + _buildInfoItem('DROP', drop), + _buildInfoItem('PACKING', packing), + ], + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildDetailSection( + icon: Icons.local_shipping_outlined, + title: 'LOGISTICS & OPS', + items: [ + _buildInfoItem('TIME IN DAYS', time), + _buildInfoItem('COMPANY', company), + _buildInfoItem('FREQUENCY', freq), + _buildInfoItem('CONTACT NUMBER', phone), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + _buildDetailSection( + icon: Icons.place_outlined, + title: 'LOCATION & PINCODE', + items: [ + Row( + children: [ + Expanded(child: _buildInfoItem('AREA / ZONE', area)), + Expanded(child: _buildInfoItem('PLUS CODE', plusCode)), + ], + ), + _buildInfoItem('SERVICEABLE PINCODES', pincodes), + _buildInfoItem('FULL ADDRESS', address), + const SizedBox(height: 8), + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.map_outlined, size: 14, color: AppColors.primary), + label: const Text('View On Map', style: TextStyle(color: AppColors.primary, fontSize: 12, fontWeight: FontWeight.w700)), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8), side: const BorderSide(color: AppColors.primary)), + ), + ) + ], + ), + const SizedBox(height: 12), + _buildDetailSection( + icon: Icons.list_alt, + title: 'RECORD METADATA', + items: [ + Row( + children: [ + Expanded(child: _buildInfoItem('CREATED BY', 'System')), + Expanded(child: _buildInfoItem('LAST EDITED BY', '—')), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildDetailSection({required IconData icon, required String title, required List items}) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: const BoxDecoration( + color: Color(0xFFFDECEE), + borderRadius: BorderRadius.vertical(top: Radius.circular(8)), + ), + child: Row( + children: [ + Icon(icon, size: 14, color: AppColors.primary), + const SizedBox(width: 6), + Expanded( + child: FittedBox( + alignment: Alignment.centerLeft, + fit: BoxFit.scaleDown, + child: Text(title, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: AppColors.darkBlue, letterSpacing: 0.5)), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items.expand((w) => [w, const SizedBox(height: 12)]).toList()..removeLast(), + ), + ), + ], + ), + ); + } + + Widget _buildInfoItem(String label, String value, {bool isHighlight = false}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 9, fontWeight: FontWeight.w800, color: AppColors.textSecondary, letterSpacing: 0.5)), + const SizedBox(height: 4), + if (isHighlight) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration(color: AppColors.green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4)), + child: Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.greenDark)), + ) + else + Text(value, style: const TextStyle(fontSize: 12, color: AppColors.darkBlue, fontWeight: FontWeight.w600)), + ], + ); + } +} diff --git a/lib/screens/update_screen.dart b/lib/screens/update_screen.dart new file mode 100644 index 0000000..1e565bd --- /dev/null +++ b/lib/screens/update_screen.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:in_app_update/in_app_update.dart'; +import 'dart:io'; +import '../theme/app_theme.dart'; + +class UpdateRequiredScreen extends StatelessWidget { + const UpdateRequiredScreen({super.key}); + + Future _triggerNativeUpdate() async { + try { + if (Platform.isAndroid) { + await InAppUpdate.performImmediateUpdate(); + } + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffold, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Illustration or Icon + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.system_update_rounded, + size: 50, + color: AppColors.primary, + ), + ), + const SizedBox(height: 32), + + // Text Content + const Text( + 'Update Required', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue, + ), + ), + const SizedBox(height: 16), + const Text( + 'A new version of Doormile CRM is available. You must update to the latest version to continue using the app securely.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + height: 1.5, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 48), + + // Update Button + SizedBox( + width: double.infinity, + height: 54, + child: ElevatedButton( + onPressed: _triggerNativeUpdate, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + 'Update Now', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..2431fc7 --- /dev/null +++ b/lib/services/auth_service.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +/// A single authenticated user. +class AuthUser { + final String email; + final String name; + final String role; + final String token; + + const AuthUser({ + required this.email, + required this.name, + required this.role, + required this.token, + }); + + Map toJson() => { + 'email': email, + 'name': name, + 'role': role, + 'token': token, + }; + + factory AuthUser.fromJson(Map json) => AuthUser( + email: json['email'] as String? ?? '', + name: json['name'] as String? ?? '', + role: json['role'] as String? ?? '', + token: json['token'] as String? ?? '', + ); +} + +class AuthService { + static const String _base = 'https://api.doormile.com/api/v1'; + + /// Login with real backend API + Future login(String email, String password) async { + try { + final res = await http.post( + Uri.parse('$_base/admin/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'email': email, 'password': password}), + ).timeout(const Duration(seconds: 15)); + + if (res.statusCode == 200) { + final data = jsonDecode(res.body); + final token = data['token'] ?? ''; + final userObj = data['user'] ?? {}; + + // Sometimes APIs return user inside 'data' or directly. Adjusting based on typical JWT responses. + final emailStr = userObj['email'] ?? email; + final nameStr = userObj['name'] ?? userObj['first_name'] ?? 'Admin'; + final roleStr = userObj['role'] ?? 'admin'; + + final user = AuthUser( + email: emailStr, + name: nameStr, + role: roleStr, + token: token, + ); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_user_data', jsonEncode(user.toJson())); + + return user; + } + return null; + } catch (_) { + return null; + } + } + + /// Restore user from local storage + Future getSavedUser() async { + try { + final prefs = await SharedPreferences.getInstance(); + final userData = prefs.getString('auth_user_data'); + if (userData != null && userData.isNotEmpty) { + return AuthUser.fromJson(jsonDecode(userData)); + } + } catch (_) {} + return null; + } +} diff --git a/lib/services/crm_api_service.dart b/lib/services/crm_api_service.dart new file mode 100644 index 0000000..d16e461 --- /dev/null +++ b/lib/services/crm_api_service.dart @@ -0,0 +1,380 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/client.dart'; + +class CrmApiService { + static const String _base = 'https://api.doormile.com/api/v1'; + + final http.Client _http; + String? _authToken; + + CrmApiService({http.Client? client}) : _http = client ?? http.Client(); + + void setToken(String token) { + _authToken = token; + } + + Future> _getHeaders() async { + final Map headers = { + 'Content-Type': 'application/json', + }; + if (_authToken != null && _authToken!.isNotEmpty) { + headers['Authorization'] = 'Bearer $_authToken'; + } else { + // Fallback to shared preferences just in case + try { + final prefs = await SharedPreferences.getInstance(); + final userData = prefs.getString('auth_user_data'); + if (userData != null && userData.isNotEmpty) { + final token = jsonDecode(userData)['token']?.toString(); + if (token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + } + } catch (_) {} + } + return headers; + } + + // ── Public API ────────────────────────────────────────────────────────────── + + /// Fetch all clients from the backend. + /// Throws on network failure so the caller can distinguish "backend down" + /// from "backend returned an empty list". + Future> loadClients() async { + try { + final res = await _http + .get(Uri.parse('$_base/crm/clients?limit=1000'), headers: await _getHeaders()) + .timeout(const Duration(seconds: 10)); + if (res.statusCode != 200) { + debugPrint('loadClients failed: ${res.statusCode} ${res.body}'); + return []; + } + final decoded = jsonDecode(res.body); + List body = []; + if (decoded is List) { + body = decoded; + } else if (decoded is Map && decoded['data'] is List) { + body = decoded['data']; + } + return body.map((j) => _fromJson(j as Map)).toList(); + } catch (e) { + debugPrint('loadClients exception: $e'); + return []; + } + } + + /// Create a new client on the backend. Returns the server-assigned record, + /// or null if the call fails (caller keeps the locally-created record). + Future createClient(Client client) async { + try { + final res = await _http + .post( + Uri.parse('$_base/crm/clients'), + headers: await _getHeaders(), + body: jsonEncode(_toCreatePayload(client)), + ) + .timeout(const Duration(seconds: 10)); + if (res.statusCode != 201) return null; + return _fromJson(jsonDecode(res.body) as Map); + } catch (_) { + return null; + } + } + + /// Update an existing client. `client.id` must be a valid numeric backend ID. + Future updateClient(Client client) async { + final numId = int.tryParse(client.id); + if (numId == null) return null; // temp local ID — not yet persisted + try { + final res = await _http + .put( + Uri.parse('$_base/crm/clients/$numId'), + headers: await _getHeaders(), + body: jsonEncode(_toUpdatePayload(client)), + ) + .timeout(const Duration(seconds: 10)); + if (res.statusCode != 200) return null; + return _fromJson(jsonDecode(res.body) as Map); + } catch (_) { + return null; + } + } + + /// Delete a client by ID. Returns true if successful. + Future deleteClient(String clientId) async { + final numId = int.tryParse(clientId); + if (numId == null) return false; + try { + final res = await _http + .delete( + Uri.parse('$_base/crm/clients/$numId'), + headers: await _getHeaders(), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200; + } catch (_) { + return false; + } + } + + /// Fetch all competitor branch surveys + Future> fetchSurveys() async { + try { + final res = await _http + .get(Uri.parse('$_base/admin/competitor-branches?limit=1000'), headers: await _getHeaders()) + .timeout(const Duration(seconds: 10)); + if (res.statusCode != 200) return []; + final data = jsonDecode(res.body); + if (data is List) return data; + if (data is Map && data['data'] is List) return data['data']; + return []; + } catch (_) { + return []; + } + } + + /// Fetch carrier pricing + Future> fetchPricing() async { + try { + final res = await _http + .get(Uri.parse('$_base/admin/carrier-pricing?limit=1000'), headers: await _getHeaders()) + .timeout(const Duration(seconds: 10)); + if (res.statusCode != 200) return []; + final data = jsonDecode(res.body); + if (data is List) return data; + if (data is Map && data['data'] is List) return data['data']; + return []; + } catch (_) { + return []; + } + } + + /// Create carrier pricing + Future createPricing(Map data) async { + try { + final res = await _http + .post( + Uri.parse('$_base/admin/carrier-pricing'), + headers: await _getHeaders(), + body: jsonEncode({ + 'company': data['company'], + 'weight_slab': data['weight_slab'], + 'zone': data['zone'] ?? '', + 'service_type': data['service_type'] ?? '', + 'rate': data['rate'].toString(), + }), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200 || res.statusCode == 201 || res.statusCode == 204; + } catch (_) { + return false; + } + } + + /// Update carrier pricing + Future updatePricing(String id, Map data) async { + try { + final res = await _http + .put( + Uri.parse('$_base/admin/carrier-pricing/$id'), + headers: await _getHeaders(), + body: jsonEncode({ + 'company': data['company'], + 'weight_slab': data['weight_slab'], + 'zone': data['zone'] ?? '', + 'service_type': data['service_type'] ?? '', + 'rate': data['rate'].toString(), + }), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200 || res.statusCode == 204; + } catch (_) { + return false; + } + } + + /// Delete carrier pricing + Future deletePricing(String id) async { + try { + final res = await _http + .delete( + Uri.parse('$_base/admin/carrier-pricing/$id'), + headers: await _getHeaders(), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200 || res.statusCode == 204; + } catch (_) { + return false; + } + } + + /// Create competitor branch (survey) + Future createSurvey(Map data) async { + try { + final res = await _http + .post( + Uri.parse('$_base/admin/competitor-branches'), + headers: await _getHeaders(), + body: jsonEncode(data), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200 || res.statusCode == 201 || res.statusCode == 204; + } catch (_) { + return false; + } + } + + /// Update competitor branch (survey) + Future updateSurvey(String id, Map data) async { + try { + final res = await _http + .put( + Uri.parse('$_base/admin/competitor-branches/$id'), + headers: await _getHeaders(), + body: jsonEncode(data), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200 || res.statusCode == 204; + } catch (_) { + return false; + } + } + + /// Delete competitor branch (survey) + Future deleteSurvey(String id) async { + try { + final res = await _http + .delete( + Uri.parse('$_base/admin/competitor-branches/$id'), + headers: await _getHeaders(), + ) + .timeout(const Duration(seconds: 10)); + return res.statusCode == 200 || res.statusCode == 204; + } catch (_) { + return false; + } + } + + // ── Serialisation ─────────────────────────────────────────────────────────── + + Client _fromJson(Map j) => Client( + id: j['id'].toString(), + name: _joinName(j['first_name'], j['last_name']), + city: j['city'] as String? ?? '', + businessType: _parseBusinessType(j['businessType'] as String?), + parcelVolume: (j['parcelVolume'] as num?)?.toDouble() ?? 0.0, + activeContracts: j['activeContracts'] as int? ?? 0, + provider: j['provider'] as String? ?? '', + efficiency: j['efficiency'] as String? ?? '', + status: _parseStatus(j['status'] as String?), + lastUpdated: j['lastUpdated'] as String? ?? '', + surveySubmitted: true, // in DB = was submitted + notes: j['notes'] as String? ?? '', + dataConsent: _parseConsent(j['dataConsent'] as String?), + phone: j['phone'] as String? ?? '', + frequency: j['frequency'] as String? ?? '', + businessState: j['businessState'] as String? ?? '', + transitFrom: j['transitFrom'] as String? ?? '', + transitTo: j['transitTo'] as String? ?? '', + neighbourhood: j['neighbourhood'] as String? ?? '', + logisticsSegment: j['logisticsSegment'] as String? ?? '', + pincode: j['pincode'] as String? ?? '', + surveyLat: (j['survey_lat'] as num?)?.toDouble() ?? 0.0, + surveyLng: (j['survey_long'] as num?)?.toDouble() ?? 0.0, + surveyAddress: j['surveyAddress'] as String? ?? '', + surveyZone: j['surveyZone'] as String? ?? '', + surveyPincode: j['surveyPincode'] as String? ?? '', + recordedByName: '', + recordedByEmail: j['email'] as String? ?? '', + recordedByRole: j['role'] as String? ?? '', + ); + + Map _toCreatePayload(Client c) => { + 'first_name': c.name, + 'phone': c.phone, + 'city': c.city, + 'businessState': c.businessState, + 'businessType': c.businessType.name, + 'status': c.status.name, + 'frequency': c.frequency, + 'parcelVolume': c.parcelVolume, + 'activeContracts': c.activeContracts, + 'provider': c.provider, + 'efficiency': c.efficiency, + 'logisticsSegment': c.logisticsSegment, + 'transitFrom': c.transitFrom, + 'transitTo': c.transitTo, + 'neighbourhood': c.neighbourhood, + 'pincode': c.pincode, + 'surveyAddress': c.surveyAddress, + 'surveyZone': c.surveyZone, + 'surveyPincode': c.surveyPincode, + 'survey_lat': c.surveyLat, + 'survey_long': c.surveyLng, + 'notes': c.notes, + 'dataConsent': c.dataConsent.name, + 'registration_source': 'mobile', + }; + + Map _toUpdatePayload(Client c) => { + 'first_name': c.name, + 'phone': c.phone, + 'city': c.city, + 'businessState': c.businessState, + 'businessType': c.businessType.name, + 'status': c.status.name, + 'frequency': c.frequency, + 'parcelVolume': c.parcelVolume, + 'activeContracts': c.activeContracts, + 'provider': c.provider, + 'efficiency': c.efficiency, + 'logisticsSegment': c.logisticsSegment, + 'transitFrom': c.transitFrom, + 'transitTo': c.transitTo, + 'neighbourhood': c.neighbourhood, + 'pincode': c.pincode, + 'surveyAddress': c.surveyAddress, + 'surveyZone': c.surveyZone, + 'surveyPincode': c.surveyPincode, + 'survey_lat': c.surveyLat, + 'survey_long': c.surveyLng, + 'notes': c.notes, + 'dataConsent': c.dataConsent.name, + }; + + // ── Helpers ───────────────────────────────────────────────────────────────── + + String _joinName(dynamic first, dynamic last) { + final f = (first as String?) ?? ''; + final l = (last as String?) ?? ''; + final joined = [f, l].where((s) => s.isNotEmpty).join(' '); + return joined.isEmpty ? '—' : joined; + } + + BusinessType _parseBusinessType(String? s) { + if (s == null) return BusinessType.retail; + return BusinessType.values.firstWhere( + (b) => b.name == s, + orElse: () => BusinessType.retail, + ); + } + + ClientStatus _parseStatus(String? s) { + if (s == null) return ClientStatus.newClient; + return ClientStatus.values.firstWhere( + (st) => st.name == s, + orElse: () => ClientStatus.newClient, + ); + } + + DataConsent _parseConsent(String? s) { + if (s == null) return DataConsent.full; + return DataConsent.values.firstWhere( + (d) => d.name == s, + orElse: () => DataConsent.full, + ); + } +} diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart new file mode 100644 index 0000000..217e47c --- /dev/null +++ b/lib/services/location_service.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; +import 'package:geolocator/geolocator.dart'; +import 'package:http/http.dart' as http; + +/// Why location cannot be captured (used to show the right alert). +enum LocationCheckResult { + ready, + serviceDisabled, // device GPS/location is switched off + permissionDenied, // user denied — can ask again + permissionPermanentlyDenied, // "Don't ask again" was checked +} + +/// GPS coordinates + reverse-geocoded details captured at survey submission. +class SurveyLocation { + final double lat; + final double lng; + final String address; + final String zone; // suburb / neighbourhood + final String city; // detected city / town + final String state; // detected state + final String pincode; // postal code from reverse-geocode + + const SurveyLocation({ + required this.lat, + required this.lng, + required this.address, + required this.zone, + this.city = '', + this.state = '', + this.pincode = '', + }); + + factory SurveyLocation.empty() => + const SurveyLocation(lat: 0.0, lng: 0.0, address: '', zone: ''); + + bool get hasFix => lat != 0.0 || lng != 0.0; +} + +class LocationService { + /// Checks service + permission WITHOUT triggering a system prompt. + /// Use this to decide whether to show an in-app alert before capturing. + static Future checkStatus() async { + if (!await Geolocator.isLocationServiceEnabled()) { + return LocationCheckResult.serviceDisabled; + } + final perm = await Geolocator.checkPermission(); + if (perm == LocationPermission.deniedForever) { + return LocationCheckResult.permissionPermanentlyDenied; + } + if (perm == LocationPermission.denied) { + return LocationCheckResult.permissionDenied; + } + return LocationCheckResult.ready; + } + + /// Requests permission (one-shot OS prompt) then captures GPS + address. + /// Never throws — returns [SurveyLocation.empty] on any failure. + static Future capture() async { + try { + if (!await Geolocator.isLocationServiceEnabled()) { + return SurveyLocation.empty(); + } + + var perm = await Geolocator.checkPermission(); + if (perm == LocationPermission.denied) { + perm = await Geolocator.requestPermission(); + } + if (perm == LocationPermission.denied || + perm == LocationPermission.deniedForever) { + return SurveyLocation.empty(); + } + + final position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + ), + ).timeout(const Duration(seconds: 10)); + + final geo = await _reverseGeocode(position.latitude, position.longitude); + + return SurveyLocation( + lat: position.latitude, + lng: position.longitude, + address: geo['display_name'] ?? '', + zone: geo['zone'] ?? '', + city: geo['city'] ?? '', + state: geo['state'] ?? '', + pincode: geo['pincode'] ?? '', + ); + } catch (_) { + return SurveyLocation.empty(); + } + } + + /// Opens the device's Location / GPS settings page. + static Future openLocationSettings() => + Geolocator.openLocationSettings(); + + /// Opens this app's permission settings page (for permanently-denied case). + static Future openAppSettings() => Geolocator.openAppSettings(); + + static Future> _reverseGeocode( + double lat, double lng) async { + try { + final uri = Uri.parse( + 'https://nominatim.openstreetmap.org/reverse' + '?lat=$lat&lon=$lng&format=json&addressdetails=1', + ); + final res = await http + .get(uri, headers: {'User-Agent': 'DoorMile-CRM/1.0'}) + .timeout(const Duration(seconds: 6)); + + if (res.statusCode != 200) return {}; + + final body = jsonDecode(res.body) as Map; + final addr = (body['address'] as Map?) ?? {}; + + final zone = (addr['suburb'] ?? + addr['neighbourhood'] ?? + addr['quarter'] ?? + addr['village'] ?? + '') as String; + + final city = (addr['city'] ?? + addr['town'] ?? + addr['municipality'] ?? + addr['county'] ?? + addr['state_district'] ?? + '') as String; + + final state = (addr['state'] ?? '') as String; + + final pincode = (addr['postcode'] ?? '') as String; + + return { + 'display_name': (body['display_name'] as String?) ?? '', + 'zone': zone, + 'city': city, + 'state': state, + 'pincode': pincode, + }; + } catch (_) { + return {}; + } + } +} diff --git a/lib/services/qdrant_service.dart b/lib/services/qdrant_service.dart new file mode 100644 index 0000000..3b51a0b --- /dev/null +++ b/lib/services/qdrant_service.dart @@ -0,0 +1,235 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../models/client.dart'; + +class QdrantService { + static const String _baseUrl = 'http://66.116.207.225:6333'; + static const String _apiKey = 'Package@321#'; + + static const String collectionName = 'doormile_clients'; + + // 16-dim feature vector (city-agnostic — city stored in payload only) + // + // [0] parcelVolume (normalised 0–1 over max 1 200) + // [1] activeContracts (normalised 0–1 over max 40) + // [2–5] businessType one-hot (ecommerce, manufacturing, wholesale, retail) + // [6–9] status one-hot (newClient, pending, completed, updated) + // [10] surveySubmitted (0 / 1) + // [11] dataConsent basicOnly=0, full=1 + // [12] hasPhone (0 / 1) + // [13] hasTransitRoute both from+to non-empty (0 / 1) + // [14] frequencyScore none=0, monthly=0.25, biweekly=0.5, weekly=0.75, daily=1 + // [15] hasNeighbourhood (0 / 1) + static const int _vectorSize = 16; + + final http.Client _http; + QdrantService({http.Client? client}) : _http = client ?? http.Client(); + + Map get _headers => { + 'Content-Type': 'application/json', + 'api-key': _apiKey, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }; + + /// Creates the collection if it does not already exist. + Future ensureCollection() async { + final res = await _http.get( + Uri.parse('$_baseUrl/collections/$collectionName'), + headers: _headers, + ); + if (res.statusCode == 200) return; + + await _http.put( + Uri.parse('$_baseUrl/collections/$collectionName'), + headers: _headers, + body: jsonEncode({ + 'vectors': {'size': _vectorSize, 'distance': 'Cosine'}, + }), + ); + } + + /// Upsert (insert or update) a single client. + Future upsertClient(Client client) async { + await _http.put( + Uri.parse('$_baseUrl/collections/$collectionName/points'), + headers: _headers, + body: jsonEncode({ + 'points': [_toPoint(client)], + }), + ); + } + + /// Batch upsert a list of clients. + Future upsertClients(List clients) async { + if (clients.isEmpty) return; + await _http.put( + Uri.parse('$_baseUrl/collections/$collectionName/points'), + headers: _headers, + body: jsonEncode({ + 'points': clients.map(_toPoint).toList(), + }), + ); + } + + /// Fetch all stored clients from Qdrant (up to 500). + Future> loadClients() async { + final res = await _http.post( + Uri.parse('$_baseUrl/collections/$collectionName/points/scroll'), + headers: _headers, + body: jsonEncode({ + 'limit': 500, + 'with_payload': true, + 'with_vector': false, + }), + ); + if (res.statusCode != 200) return []; + final body = jsonDecode(res.body) as Map; + final points = (body['result']?['points'] as List?) ?? []; + return points.map((p) => _fromPoint(p as Map)).toList(); + } + + /// Drop and recreate the collection — used for staging resets. + Future deleteCollection() async { + await _http.delete( + Uri.parse('$_baseUrl/collections/$collectionName'), + headers: _headers, + ); + } + + /// Delete a client by its string ID. + Future deleteClient(String clientId) async { + await _http.post( + Uri.parse('$_baseUrl/collections/$collectionName/points/delete'), + headers: _headers, + body: jsonEncode({ + 'points': [_stableId(clientId)], + }), + ); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + int _stableId(String id) { + int h = 0; + for (final c in id.codeUnits) { + h = (h * 31 + c) & 0x7FFFFFFF; + } + return h == 0 ? 1 : h; + } + + Map _toPoint(Client client) => { + 'id': _stableId(client.id), + 'vector': _toVector(client), + 'payload': { + 'clientId': client.id, + 'name': client.name, + 'city': client.city, + 'businessType': client.businessType.name, + 'parcelVolume': client.parcelVolume, + 'activeContracts': client.activeContracts, + 'provider': client.provider, + 'efficiency': client.efficiency, + 'status': client.status.name, + 'lastUpdated': client.lastUpdated, + 'surveySubmitted': client.surveySubmitted, + 'notes': client.notes, + 'dataConsent': client.dataConsent.name, + 'phone': client.phone, + 'frequency': client.frequency, + 'businessState': client.businessState, + 'transitType': client.transitType, + 'transitFrom': client.transitFrom, + 'transitTo': client.transitTo, + 'neighbourhood': client.neighbourhood, + 'logisticsSegment': client.logisticsSegment, + 'pincode': client.pincode, + // GPS captured at submission — not shown in app, stored for field audit + 'surveyLat': client.surveyLat, + 'surveyLng': client.surveyLng, + 'surveyAddress': client.surveyAddress, + 'surveyZone': client.surveyZone, + 'surveyPincode': client.surveyPincode, + // Field-agent attribution — who collected this survey (login session) + 'recordedByName': client.recordedByName, + 'recordedByEmail': client.recordedByEmail, + 'recordedByRole': client.recordedByRole, + // Qdrant geo payload — enables geo-radius queries later + if (client.surveyLat != 0.0 || client.surveyLng != 0.0) + 'surveyGeo': {'lat': client.surveyLat, 'lon': client.surveyLng}, + }, + }; + + List _toVector(Client client) { + final v = List.filled(_vectorSize, 0.0); + v[0] = (client.parcelVolume / 1200.0).clamp(0.0, 1.0); + v[1] = (client.activeContracts / 40.0).clamp(0.0, 1.0); + v[2 + client.businessType.index] = 1.0; + v[6 + client.status.index] = 1.0; + v[10] = client.surveySubmitted ? 1.0 : 0.0; + v[11] = client.dataConsent == DataConsent.full ? 1.0 : 0.0; + v[12] = client.phone.isNotEmpty ? 1.0 : 0.0; + v[13] = (client.transitFrom.isNotEmpty && client.transitTo.isNotEmpty) + ? 1.0 + : 0.0; + v[14] = _frequencyScore(client.frequency); + v[15] = client.neighbourhood.isNotEmpty ? 1.0 : 0.0; + return v; + } + + double _frequencyScore(String freq) { + switch (freq.toLowerCase()) { + case 'daily': + return 1.0; + case 'weekly': + return 0.75; + case 'bi-weekly': + return 0.5; + case 'monthly': + return 0.25; + default: + return 0.0; + } + } + + Client _fromPoint(Map point) { + final p = point['payload'] as Map; + return Client( + id: p['clientId'] as String, + name: p['name'] as String, + city: p['city'] as String, + businessType: + BusinessType.values.firstWhere((b) => b.name == p['businessType']), + parcelVolume: (p['parcelVolume'] as num?)?.toDouble() ?? 0.0, + activeContracts: p['activeContracts'] as int, + provider: p['provider'] as String, + efficiency: p['efficiency'] as String, + status: ClientStatus.values.firstWhere((s) => s.name == p['status']), + lastUpdated: p['lastUpdated'] as String, + surveySubmitted: p['surveySubmitted'] as bool, + notes: (p['notes'] as String?) ?? '', + dataConsent: DataConsent.values.firstWhere( + (d) => d.name == (p['dataConsent'] as String? ?? 'full'), + orElse: () => DataConsent.full, + ), + phone: (p['phone'] as String?) ?? '', + frequency: (p['frequency'] as String?) ?? '', + businessState: (p['businessState'] as String?) ?? '', + transitType: (p['transitType'] as String?) ?? '', + transitFrom: (p['transitFrom'] as String?) ?? '', + transitTo: (p['transitTo'] as String?) ?? '', + neighbourhood: (p['neighbourhood'] as String?) ?? '', + logisticsSegment: (p['logisticsSegment'] as String?) ?? '', + pincode: (p['pincode'] as String?) ?? '', + surveyLat: (p['surveyLat'] as num?)?.toDouble() ?? 0.0, + surveyLng: (p['surveyLng'] as num?)?.toDouble() ?? 0.0, + surveyAddress: (p['surveyAddress'] as String?) ?? '', + surveyZone: (p['surveyZone'] as String?) ?? '', + surveyPincode: (p['surveyPincode'] as String?) ?? '', + recordedByName: (p['recordedByName'] as String?) ?? '', + recordedByEmail: (p['recordedByEmail'] as String?) ?? '', + recordedByRole: (p['recordedByRole'] as String?) ?? '', + ); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..fe2a4ca --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,361 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppColors { + static const primary = Color(0xFF8D0012); + static const primaryDark = Color(0xFFB51621); + static const darkBlue = Color(0xFF0B1C30); + static const lightBg = Color(0xFFF8F9FF); + static const border = Color(0xFFE4BEBA); + static const textSecondary = Color(0xFF5B403E); + static const green = Color(0xFF006760); + static const greenDark = Color(0xFF00504A); + static const blueAccent = Color(0xFFD3E4FE); + static const blueLight = Color(0xFFEFF4FF); + static const muted = Color(0xFF8F6F6D); + static const surface = Color(0xFFFFFFFF); + static const scaffold = Color(0xFFF1F5F9); +} + +ThemeData buildAppTheme() { + return ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + primary: AppColors.primary, + surface: AppColors.lightBg, + ), + scaffoldBackgroundColor: AppColors.scaffold, + textTheme: GoogleFonts.interTextTheme().copyWith( + displayLarge: GoogleFonts.inter( + fontWeight: FontWeight.w800, + color: AppColors.darkBlue, + ), + bodyMedium: GoogleFonts.inter(color: AppColors.darkBlue), + ), + appBarTheme: AppBarTheme( + backgroundColor: AppColors.surface, + surfaceTintColor: Colors.transparent, + elevation: 0, + shadowColor: Colors.black12, + scrolledUnderElevation: 1, + titleTextStyle: GoogleFonts.inter( + fontWeight: FontWeight.w800, + fontSize: 16, + color: AppColors.darkBlue, + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: AppColors.surface, + indicatorColor: AppColors.primaryDark, + labelTextStyle: WidgetStateProperty.resolveWith((states) { + final active = states.contains(WidgetState.selected); + return GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w700, + color: active ? AppColors.primary : AppColors.muted, + ); + }), + iconTheme: WidgetStateProperty.resolveWith((states) { + final active = states.contains(WidgetState.selected); + return IconThemeData( + color: active ? Colors.white : AppColors.muted, + size: 18, + ); + }), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary, width: 1.5), + ), + filled: true, + fillColor: AppColors.surface, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + ), + ); +} + +class WideTopBanner extends StatefulWidget { + final bool backendReady; + final String subtitle; + final VoidCallback? onLogout; + + const WideTopBanner({ + super.key, + required this.backendReady, + required this.subtitle, + this.onLogout, + }); + + @override + State createState() => _WideTopBannerState(); +} + +class _WideTopBannerState extends State { + String _title = 'Doormile CRM'; + + @override + void initState() { + super.initState(); + _loadUser(); + } + + Future _loadUser() async { + try { + final prefs = await SharedPreferences.getInstance(); + final name = prefs.getString('user_name'); + if (name != null && name.isNotEmpty && mounted) { + setState(() => _title = 'Hi, $name šŸ‘‹'); + } + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + Color(0xFFF8FAFC), + ], + ), + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: AppColors.darkBlue.withValues(alpha: 0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.04), + blurRadius: 20, + offset: const Offset(0, 10), + spreadRadius: -5, + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(2.5), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + AppColors.primary.withValues(alpha: 0.8), + AppColors.border, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withValues(alpha: 0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: ClipOval( + child: Image.asset( + 'assets/doormilelogoround.png', + width: 38, + height: 38, + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: AppColors.darkBlue, + letterSpacing: -0.3, + ), + ), + const SizedBox(height: 3), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + widget.subtitle.toUpperCase(), + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.w800, + letterSpacing: 1.2, + color: AppColors.primary, + ), + ), + ), + ], + ), + ), + _SyncStatusBadge(ready: widget.backendReady), + if (widget.onLogout != null) ...[ + const SizedBox(width: 8), + GestureDetector( + onTap: widget.onLogout, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: AppColors.border.withValues(alpha: 0.2)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.02), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: const Icon( + Icons.logout_rounded, + size: 20, + color: AppColors.primary, + ), + ), + ), + ], + ], + ), + ); + } +} + +class _SyncStatusBadge extends StatelessWidget { + final bool ready; + const _SyncStatusBadge({required this.ready}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: ready ? const Color(0xFFECFDF5) : const Color(0xFFFFFBEB), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: ready ? const Color(0xA110B981) : const Color(0xA1FBBF24), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: ready + ? const Color(0xFF10B981).withValues(alpha: 0.1) + : const Color(0xFFF59E0B).withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _PulsingDot( + color: ready ? const Color(0xFF10B981) : const Color(0xFFF59E0B), + ), + const SizedBox(width: 6), + Text( + ready ? 'SYNCED' : 'CACHED', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w800, + color: ready ? const Color(0xFF065F46) : const Color(0xFF92400E), + letterSpacing: 0.5, + ), + ), + ], + ), + ); + } +} + +class _PulsingDot extends StatefulWidget { + final Color color; + const _PulsingDot({required this.color}); + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: 6, + height: 6, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.color, + boxShadow: [ + BoxShadow( + color: widget.color.withValues(alpha: 0.6 * _animation.value), + blurRadius: 6 * _animation.value, + spreadRadius: 2 * _animation.value, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/utils/snackbar_utils.dart b/lib/utils/snackbar_utils.dart new file mode 100644 index 0000000..0d16cf2 --- /dev/null +++ b/lib/utils/snackbar_utils.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import '../theme/app_theme.dart'; + +class SnackbarUtils { + static void showSuccess(BuildContext context, String message) { + _showSnackBar( + context, + message, + icon: Icons.check_circle_outline, + backgroundColor: AppColors.greenDark, + ); + } + + static void showError(BuildContext context, String message) { + _showSnackBar( + context, + message, + icon: Icons.error_outline, + backgroundColor: AppColors.primaryDark, + ); + } + + static void showWarning(BuildContext context, String message) { + _showSnackBar( + context, + message, + icon: Icons.warning_amber_rounded, + backgroundColor: const Color(0xFFE87C00), // Orange + ); + } + + static void _showSnackBar( + BuildContext context, + String message, { + required IconData icon, + required Color backgroundColor, + }) { + if (!context.mounted) return; + + final snackBar = SnackBar( + elevation: 0, + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.transparent, + padding: EdgeInsets.zero, + content: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: backgroundColor.withValues(alpha: 0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 22), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ), + ), + duration: const Duration(seconds: 3), + margin: const EdgeInsets.only(bottom: 24, left: 16, right: 16), + ); + + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(snackBar); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..19fbe3d --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "doormile_flutter") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.doormile.doormile_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..be1ee3e --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..4843d35 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "doormile_flutter"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "doormile_flutter"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..de906b1 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import geolocator_apple +import shared_preferences_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8388313 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* doormile_flutter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "doormile_flutter.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* doormile_flutter.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* doormile_flutter.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doormile_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doormile_flutter"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doormile_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doormile_flutter"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/doormile_flutter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/doormile_flutter"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e679f76 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..b6ccdb3 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = doormile_flutter + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.doormile.doormileFlutter + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright Ā© 2026 com.doormile. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..2ed89b4 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,642 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2 + url: "https://pub.dev" + source: hosted + version: "13.0.4" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "62ae9bb76d02526c7c2110a19b6e6ad788fe28d35e553e35efb02a41a46ab43a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + in_app_update: + dependency: "direct main" + description: + name: in_app_update + sha256: "9924a3efe592e1c0ec89dda3683b3cfec3d4cd02d908e6de00c24b759038ddb1" + url: "https://pub.dev" + source: hosted + version: "4.2.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..24313a8 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,39 @@ +name: doormile_flutter +description: "Doormile CRM - Flutter App" +publish_to: 'none' +version: 1.0.7+8 + +environment: + sdk: ^3.10.7 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + google_fonts: ^6.2.1 + http: ^1.2.0 + geolocator: ^13.0.0 + shared_preferences: ^2.5.5 + in_app_update: ^4.2.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.13.1 + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + remove_alpha_ios: true + background_color_ios: "#FFFFFF" + image_path: "assets/doormilelogoround.png" + adaptive_icon_background: "#FFFFFF" + adaptive_icon_foreground: "assets/doormile_round_fg.png" + +flutter: + uses-material-design: true + assets: + - assets/preloader.png + - assets/doormilelogo512512.png + - assets/doormilelogoround.png diff --git a/qdrant/README.md b/qdrant/README.md new file mode 100644 index 0000000..7b4efc9 --- /dev/null +++ b/qdrant/README.md @@ -0,0 +1,28 @@ +# Qdrant Production Setup Guide + +## 1. How to run this on your Ubuntu VPS +1. Copy this entire `qdrant` folder to your Ubuntu server. +2. SSH into your server and navigate to this folder. +3. Run the following command to start the database in the background: + ```bash + docker compose up -d + ``` + +## 2. Accessing the UI Dashboard +Qdrant comes with a built-in, beautiful dashboard. +Once the container is running, open your web browser and go to: +`http://:6333/dashboard` + +Here you can: +- See all your Collections. +- View clusters and nodes. +- Visually inspect your vector payloads and data. + +## 3. Data Persistence +We have configured two persistent volumes: +- `./storage`: This is where the live database lives. Even if the server restarts, data is safe here. +- `./snapshots`: Use this folder to store manual database backups. + +## 4. Security Warning (Crucial) +By default, port `6333` is open. You **must** configure your Ubuntu UFW firewall so that only your backend server (e.g., your Node.js or Python API server) can access this port. +Do NOT leave it fully open to the public internet, or anyone can access your vectors. diff --git a/qdrant/docker-compose.yml b/qdrant/docker-compose.yml new file mode 100644 index 0000000..f6c7a51 --- /dev/null +++ b/qdrant/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + qdrant: + image: qdrant/qdrant:latest + container_name: qdrant_production + restart: unless-stopped + ports: + # REST API & Web Dashboard (Accessible at http://:6333/dashboard) + - "6333:6333" + # gRPC API (For faster, low-latency communication with backends) + - "6334:6334" + volumes: + # Persistent Volume: Ensures data is never lost on restarts + - ./storage:/qdrant/storage + # Snapshots Volume: Dedicated folder for backups + - ./snapshots:/qdrant/snapshots + environment: + # Disable telemetry for privacy + - QDRANT__TELEMETRY_DISABLED=true + - QDRANT__SERVICE__API_KEY=Package@321# + ulimits: + # Professional tuning: Vector DBs need high open file limits for disk I/O + nofile: + soft: 65535 + hard: 65535 + deploy: + resources: + limits: + # Memory Limit: Prevents Qdrant from taking 100% of your VPS RAM. + # Adjust '4G' based on your actual VPS specs. + memory: 4G diff --git a/qdrant/migrate.py b/qdrant/migrate.py new file mode 100644 index 0000000..9fca122 --- /dev/null +++ b/qdrant/migrate.py @@ -0,0 +1,84 @@ +import os +from qdrant_client import QdrantClient +from qdrant_client.models import PointStruct + +# ========================================== +# 1. SETUP YOUR CONNECTIONS +# ========================================== +# Connect to your old Qdrant Cloud (Source) +cloud_client = QdrantClient( + url="https://369d1a18-9eaa-4bf9-be05-115c9f188387.us-west-1-0.aws.cloud.qdrant.io:6333", + api_key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIiwic3ViamVjdCI6ImFwaS1rZXk6MDY3MmFiZDktOTRhZi00NWMyLWJhZDctNWZlNmI2MTRiZDBkIn0.z_En6ORE7oHyGRxBAAjkho8-b-cqTXbWOPUassDfbYw" +) + +# Connect to your new VPS Server (Destination) +vps_client = QdrantClient(url="http://66.116.207.225:6333") + +# We will loop through both of your collections! +COLLECTIONS_TO_MIGRATE = ["doormile_clients", "doormile_auth"] + +for collection_name in COLLECTIONS_TO_MIGRATE: + print(f"\n==========================================") + print(f"šŸš€ Starting migration for: {collection_name}") + print(f"==========================================") + + # ========================================== + # 2. RECREATE COLLECTION ON VPS + # ========================================== + try: + collection_info = cloud_client.get_collection(collection_name=collection_name) + except Exception as e: + print(f"Could not find collection '{collection_name}' on cloud. Skipping...") + continue + + print(f"Recreating '{collection_name}' on your VPS with identical settings...") + vps_client.recreate_collection( + collection_name=collection_name, + vectors_config=collection_info.config.params.vectors, + ) + + # ========================================== + # 3. TRANSFER THE DATA IN BATCHES + # ========================================== + batch_size = 100 + offset = None + total_migrated = 0 + + print("Starting data transfer...") + + while True: + # Fetch a batch of points from the cloud + records, offset = cloud_client.scroll( + collection_name=collection_name, + limit=batch_size, + offset=offset, + with_payload=True, + with_vectors=True + ) + + if not records: + break + + # Convert records to PointStruct format + points = [ + PointStruct( + id=record.id, + vector=record.vector, + payload=record.payload + ) + for record in records + ] + + # Upload the batch to your VPS + vps_client.upsert( + collection_name=collection_name, + points=points + ) + + total_migrated += len(points) + print(f" -> Copied {total_migrated} vectors...") + + if offset is None: + break + +print("\nāœ… Migration Complete! Both collections are safely on your VPS.") diff --git a/qdrant/qdrant_to_postgres.py b/qdrant/qdrant_to_postgres.py new file mode 100644 index 0000000..05b9d5a --- /dev/null +++ b/qdrant/qdrant_to_postgres.py @@ -0,0 +1,126 @@ +import psycopg2 +from qdrant_client import QdrantClient +from psycopg2.extras import execute_values +import json + +# ========================================== +# 1. SETUP YOUR CONNECTIONS +# ========================================== + +# Connect to your Qdrant (using your Cloud connection details with API Key) +print("šŸ”Œ Connecting to Qdrant...") +qdrant_client = QdrantClient( + url="https://369d1a18-9eaa-4bf9-be05-115c9f188387.us-west-1-0.aws.cloud.qdrant.io:6333", + api_key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIiwic3ViamVjdCI6ImFwaS1rZXk6MDY3MmFiZDktOTRhZi00NWMyLWJhZDctNWZlNmI2MTRiZDBkIn0.z_En6ORE7oHyGRxBAAjkho8-b-cqTXbWOPUassDfbYw" +) + +# Connect to your PostgreSQL Database (Credentials from your .env) +print("šŸ”Œ Connecting to PostgreSQL...") +try: + pg_conn = psycopg2.connect( + host="31.97.228.132", + port="5433", + dbname="logistics", + user="admin", + password="Package@321#" + ) + pg_cursor = pg_conn.cursor() + print("āœ… PostgreSQL Connected!") +except Exception as e: + print(f"āŒ Failed to connect to Postgres: {e}") + exit(1) + +# ========================================== +# 2. MIGRATION FUNCTIONS +# ========================================== + +def migrate_clients(): + print("\nšŸš€ Starting migration for: doormile_clients") + collection_name = "doormile_clients" + + # Scroll through all points in Qdrant + offset = None + batch_size = 100 + total_migrated = 0 + + while True: + records, offset = qdrant_client.scroll( + collection_name=collection_name, + limit=batch_size, + offset=offset, + with_payload=True, + with_vectors=False # We don't need vectors for Postgres! + ) + + if not records: + break + + for record in records: + payload = record.payload + + # Extract fields safely from payload (Fallback to empty string if missing) + first_name = payload.get("first_name", payload.get("name", "Unknown")) + last_name = payload.get("last_name", "") + phone = payload.get("phone", "") + address = payload.get("address", "") + + # We skip survey_lat/long here because old data likely doesn't have it, + # but we can add default 0.0 values if required by the database schema. + survey_lat = payload.get("survey_lat", 0.0) + survey_long = payload.get("survey_long", 0.0) + pincode = payload.get("pincode", "") + registration_source = payload.get("registration_source", "legacy_qdrant") + + # Insert into PostgreSQL + try: + pg_cursor.execute(""" + INSERT INTO doormile_clients + (first_name, last_name, phone, address, surveylat, surveylong, pincode, registration_source, created_at, updated_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) + RETURNING id; + """, (first_name, last_name, phone, address, survey_lat, survey_long, pincode, registration_source)) + + # Fetch the newly generated Postgres ID + new_client_id = pg_cursor.fetchone()[0] + + # Now we check if there's an email/password in this same payload, + # if so, we migrate it to the auth table simultaneously! + email = payload.get("email") + password = payload.get("password") # Note: You should hash this in Go, but here we preserve legacy passwords + + if email and password: + pg_cursor.execute(""" + INSERT INTO doormile_auth + (client_id, email, password_hash, role, created_at, updated_at) + VALUES (%s, %s, %s, 'user', NOW(), NOW()) + ON CONFLICT (email) DO NOTHING; + """, (new_client_id, email, password)) + + pg_conn.commit() + total_migrated += 1 + + except Exception as e: + pg_conn.rollback() + print(f"āš ļø Error inserting record {record.id}: {e}") + + print(f" -> Migrated {total_migrated} clients to Postgres...") + + if offset is None: + break + +# ========================================== +# 3. RUN THE MIGRATION +# ========================================== + +print("\n==========================================") +print("šŸ“¦ MIGRATION INITIALIZED") +print("==========================================") + +# Execute the migration +migrate_clients() + +# Close connections +pg_cursor.close() +pg_conn.close() + +print("\nāœ… Migration Complete! All legacy data is now strictly relational in PostgreSQL.") diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..597cb4c --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,9 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:doormile_flutter/main.dart'; + +void main() { + testWidgets('App smoke test', (WidgetTester tester) async { + await tester.pumpWidget(const DoorMileApp()); + expect(find.text('Doormile CRM'), findsWidgets); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e93ccaa --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + Doormile CRM + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..433b090 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Doormile CRM", + "short_name": "Doormile CRM", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Doormile CRM - Flutter App", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..f5825f0 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(doormile_flutter LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "doormile_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..1ece8f2 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..a49b9f6 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..7aeb5e1 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.doormile" "\0" + VALUE "FileDescription", "doormile_flutter" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "doormile_flutter" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.doormile. All rights reserved." "\0" + VALUE "OriginalFilename", "doormile_flutter.exe" "\0" + VALUE "ProductName", "doormile_flutter" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..737778d --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"doormile_flutter", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_