Add app branding, splash screen, and fix modifier validation
App Branding: - New Payfrit app icon (blue gradient with white P logo) - Custom splash screen with animated logo and tagline - Android adaptive icon with foreground/background layers - iOS app icons for all required sizes - Updated launch screen backgrounds with brand colors Splash Screen Experience: - Animated logo with fade-in effect - "Order. Pay. Go." tagline with staggered animations - Restaurant list display with search functionality - Beacon scanning integration from splash - Smooth transition to menu browse Modifier Validation Fix: - Fixed validation to check ALL modifier groups (not just selected items) - Ensures required selections are enforced for nested modifier groups - Modifier groups with children now always get validated - Prevents adding items without required selections Cart Improvements: - Better modifier display in cart items - Improved line item quantity handling - Enhanced order submission flow Beacon Scanning: - Improved beacon detection reliability - Better handling of multiple beacons - Enhanced error messaging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
|
@ -1,3 +1,6 @@
|
||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
|
|
@ -5,8 +8,15 @@ plugins {
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load key.properties
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.payfrit_app"
|
namespace = "com.payfrit.app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
|
@ -20,23 +30,25 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
applicationId = "com.payfrit.app"
|
||||||
applicationId = "com.example.payfrit_app"
|
|
||||||
// You can update the following values to match your application needs.
|
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = 1
|
||||||
versionName = flutter.versionName
|
versionName = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||||
|
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.getByName("release")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
|
|
||||||
// Disable minification and shrinking to preserve beacon library
|
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
isShrinkResources = false
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="payfrit_app"
|
android:label="Payfrit"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.payfrit.app
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
|
@ -1,12 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?android:colorBackground" />
|
<item android:drawable="@android:color/black" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -1,12 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/white" />
|
<item android:drawable="@android:color/black" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.3 KiB |
4
android/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#000000</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
<!-- Theme applied to the Android Window while the process is starting -->
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
running.
|
running.
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
79
generate_icon.html
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Payfrit Icon Generator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #333;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.icon-container {
|
||||||
|
width: 1024px;
|
||||||
|
height: 1024px;
|
||||||
|
background: #000000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.icon-container img {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 50%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #666;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="label">1024x1024 Icon Preview:</div>
|
||||||
|
<canvas id="iconCanvas" width="1024" height="1024"></canvas>
|
||||||
|
<button onclick="downloadIcon()">Download Icon (Right-click canvas and Save As PNG)</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('iconCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = function() {
|
||||||
|
// Fill black background
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, 1024, 1024);
|
||||||
|
|
||||||
|
// Calculate dimensions to fit logo (90% width, centered)
|
||||||
|
const targetWidth = 1024 * 0.85;
|
||||||
|
const scale = targetWidth / img.width;
|
||||||
|
const scaledHeight = img.height * scale;
|
||||||
|
|
||||||
|
// Center the image
|
||||||
|
const x = (1024 - targetWidth) / 2;
|
||||||
|
const y = (1024 - scaledHeight) / 2;
|
||||||
|
|
||||||
|
ctx.drawImage(img, x, y, targetWidth, scaledHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = 'icon.jpg';
|
||||||
|
|
||||||
|
function downloadIcon() {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = 'payfrit_icon_1024.png';
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
icon.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -427,7 +427,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
@ -484,7 +484,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 415 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 935 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 637 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 935 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
|
@ -97,13 +97,48 @@ class Cart {
|
||||||
}
|
}
|
||||||
|
|
||||||
double get subtotal {
|
double get subtotal {
|
||||||
return lineItems
|
// Sum all non-deleted line items (root items and modifiers)
|
||||||
.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0)
|
// Root items have their base price, modifiers have their add-on prices
|
||||||
.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
|
// Modifier prices are multiplied by the root item's quantity
|
||||||
|
double total = 0.0;
|
||||||
|
|
||||||
|
// First, get all root items and their prices
|
||||||
|
final rootItems = lineItems.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0);
|
||||||
|
for (final rootItem in rootItems) {
|
||||||
|
total += rootItem.price * rootItem.quantity;
|
||||||
|
|
||||||
|
// Add all modifier prices for this root item (recursively)
|
||||||
|
total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively sum modifier prices for a parent line item
|
||||||
|
double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) {
|
||||||
|
double total = 0.0;
|
||||||
|
final children = lineItems.where(
|
||||||
|
(item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final child in children) {
|
||||||
|
// Modifier price is multiplied by root item quantity
|
||||||
|
total += child.price * rootQuantity;
|
||||||
|
// Recursively add grandchildren modifier prices
|
||||||
|
total += _sumModifierPrices(child.orderLineItemId, rootQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only include delivery fee for delivery orders (orderTypeId == 3)
|
// Only include delivery fee for delivery orders (orderTypeId == 3)
|
||||||
double get total => subtotal + (orderTypeId == 3 ? deliveryFee : 0);
|
// Sales tax rate (8.25% for California - can be made configurable per business later)
|
||||||
|
static const double taxRate = 0.0825;
|
||||||
|
|
||||||
|
// Calculate sales tax on subtotal
|
||||||
|
double get tax => subtotal * taxRate;
|
||||||
|
|
||||||
|
double get total => subtotal + tax + (orderTypeId == 3 ? deliveryFee : 0);
|
||||||
|
|
||||||
int get itemCount {
|
int get itemCount {
|
||||||
return lineItems
|
return lineItems
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
|
|
||||||
// Brief delay to let Bluetooth subsystem fully initialize
|
// Brief delay to let Bluetooth subsystem fully initialize
|
||||||
// Without this, the first scan cycle may complete immediately with no results
|
// Without this, the first scan cycle may complete immediately with no results
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
|
|
||||||
// Create regions for all known UUIDs
|
// Create regions for all known UUIDs
|
||||||
final regions = _uuidToBeaconId.keys.map((uuid) {
|
final regions = _uuidToBeaconId.keys.map((uuid) {
|
||||||
|
|
@ -131,10 +131,11 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform 5 scans of 2 seconds each to increase chance of detecting all beacons
|
// Perform up to 5 scans of 2 seconds each, but exit early if readings are consistent
|
||||||
print('[BeaconScan] 🔄 Starting 5 scan cycles of 2 seconds each');
|
print('[BeaconScan] 🔄 Starting scan cycles (max 5 x 2 seconds, early exit if stable)');
|
||||||
|
|
||||||
for (int scanCycle = 1; scanCycle <= 5; scanCycle++) {
|
bool earlyExit = false;
|
||||||
|
for (int scanCycle = 1; scanCycle <= 3; scanCycle++) {
|
||||||
// Update status message for each cycle
|
// Update status message for each cycle
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _status = _scanMessages[scanCycle - 1]);
|
setState(() => _status = _scanMessages[scanCycle - 1]);
|
||||||
|
|
@ -165,16 +166,25 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait 2 seconds for this scan cycle to collect beacon data
|
// Wait 2 seconds for this scan cycle to collect beacon data
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
await subscription.cancel();
|
await subscription.cancel();
|
||||||
|
|
||||||
|
// After 3 cycles, check if we can exit early
|
||||||
|
if (scanCycle >= 2 && _beaconRssiSamples.isNotEmpty) {
|
||||||
|
if (_canExitEarly()) {
|
||||||
|
print('[BeaconScan] ⚡ Early exit after $scanCycle cycles - readings are stable!');
|
||||||
|
earlyExit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Short pause between scan cycles
|
// Short pause between scan cycles
|
||||||
if (scanCycle < 5) {
|
if (scanCycle < 3) {
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[BeaconScan] ✔️ All scan cycles complete');
|
print('[BeaconScan] ✔️ Scan complete${earlyExit ? ' (early exit)' : ''}');
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|
@ -291,11 +301,65 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if we can exit early based on stable readings
|
||||||
|
/// Returns true if all detected beacons have consistent RSSI across scans
|
||||||
|
bool _canExitEarly() {
|
||||||
|
if (_beaconRssiSamples.isEmpty) return false;
|
||||||
|
|
||||||
|
// Need at least one beacon with 3+ readings
|
||||||
|
bool hasEnoughSamples = _beaconRssiSamples.values.any((samples) => samples.length >= 2);
|
||||||
|
if (!hasEnoughSamples) return false;
|
||||||
|
|
||||||
|
// Check if there's a clear winner (one beacon significantly stronger than others)
|
||||||
|
// or if all beacons have low variance in their readings
|
||||||
|
for (final entry in _beaconRssiSamples.entries) {
|
||||||
|
final samples = entry.value;
|
||||||
|
if (samples.length < 3) continue;
|
||||||
|
|
||||||
|
// Calculate variance
|
||||||
|
final avg = samples.reduce((a, b) => a + b) / samples.length;
|
||||||
|
final variance = samples.map((r) => (r - avg) * (r - avg)).reduce((a, b) => a + b) / samples.length;
|
||||||
|
|
||||||
|
// If variance is high (readings fluctuating a lot), keep scanning
|
||||||
|
// Variance > 50 means RSSI is jumping around too much
|
||||||
|
if (variance > 50) {
|
||||||
|
print('[BeaconScan] ⏳ Beacon ${_uuidToBeaconId[entry.key]} has high variance (${variance.toStringAsFixed(1)}), continuing scan');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have multiple beacons, check if there's a clear strongest one
|
||||||
|
if (_beaconRssiSamples.length > 1) {
|
||||||
|
final avgRssis = <String, double>{};
|
||||||
|
for (final entry in _beaconRssiSamples.entries) {
|
||||||
|
final samples = entry.value;
|
||||||
|
if (samples.isNotEmpty) {
|
||||||
|
avgRssis[entry.key] = samples.reduce((a, b) => a + b) / samples.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by RSSI descending
|
||||||
|
final sorted = avgRssis.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
|
||||||
|
// If top two beacons are within 5 dB, keep scanning for more clarity
|
||||||
|
if (sorted.length >= 2) {
|
||||||
|
final diff = sorted[0].value - sorted[1].value;
|
||||||
|
if (diff < 5) {
|
||||||
|
print('[BeaconScan] ⏳ Top 2 beacons are close (diff=${diff.toStringAsFixed(1)} dB), continuing scan');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('[BeaconScan] ✓ Readings are stable, can exit early');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
BeaconScore? _findBestBeacon(Map<String, BeaconScore> scores) {
|
BeaconScore? _findBestBeacon(Map<String, BeaconScore> scores) {
|
||||||
if (scores.isEmpty) return null;
|
if (scores.isEmpty) return null;
|
||||||
|
|
||||||
// Filter beacons that meet minimum requirements
|
// Filter beacons that meet minimum requirements
|
||||||
const minDetections = 3; // Must be seen at least 3 times
|
const minDetections = 2; // Must be seen at least 3 times
|
||||||
const minRssi = -85; // Minimum average RSSI (beacons further than ~10m will be weaker)
|
const minRssi = -85; // Minimum average RSSI (beacons further than ~10m will be weaker)
|
||||||
|
|
||||||
final qualified = scores.values.where((score) {
|
final qualified = scores.values.where((score) {
|
||||||
|
|
@ -386,6 +450,21 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
|
||||||
child: const Text('Open Settings'),
|
child: const Text('Open Settings'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Always show manual selection option during or after scan
|
||||||
|
if (_permissionsGranted) ...[
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _navigateToRestaurantSelect,
|
||||||
|
child: const Text(
|
||||||
|
'Select Restaurant Manually',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,9 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
// Find ALL modifiers (recursively) and build breadcrumb paths for leaf items only
|
// Find ALL modifiers (recursively) and build breadcrumb paths for leaf items only
|
||||||
final modifierPaths = _buildModifierPaths(rootItem.orderLineItemId);
|
final modifierPaths = _buildModifierPaths(rootItem.orderLineItemId);
|
||||||
|
|
||||||
|
// Calculate total price for this line item (root + all modifiers)
|
||||||
|
final lineItemTotal = _calculateLineItemTotal(rootItem);
|
||||||
|
|
||||||
print('[Cart] Found ${modifierPaths.length} modifier paths for this root item');
|
print('[Cart] Found ${modifierPaths.length} modifier paths for this root item');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
|
@ -367,7 +370,7 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
"\$${rootItem.price.toStringAsFixed(2)}",
|
"\$${lineItemTotal.toStringAsFixed(2)}",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -381,7 +384,32 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate the total price for a root item including all its modifiers
|
||||||
|
double _calculateLineItemTotal(OrderLineItem rootItem) {
|
||||||
|
double total = rootItem.price * rootItem.quantity;
|
||||||
|
total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively sum modifier prices for a parent line item
|
||||||
|
double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) {
|
||||||
|
double total = 0.0;
|
||||||
|
final children = _cart!.lineItems.where(
|
||||||
|
(item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final child in children) {
|
||||||
|
// Modifier price is multiplied by root item quantity
|
||||||
|
total += child.price * rootQuantity;
|
||||||
|
// Recursively add grandchildren modifier prices
|
||||||
|
total += _sumModifierPrices(child.orderLineItemId, rootQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
/// Build breadcrumb paths for all leaf modifiers
|
/// Build breadcrumb paths for all leaf modifiers
|
||||||
|
/// Excludes default items - they don't need to be shown in the cart
|
||||||
List<ModifierPath> _buildModifierPaths(int rootOrderLineItemId) {
|
List<ModifierPath> _buildModifierPaths(int rootOrderLineItemId) {
|
||||||
final paths = <ModifierPath>[];
|
final paths = <ModifierPath>[];
|
||||||
|
|
||||||
|
|
@ -394,13 +422,19 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
|
|
||||||
// Recursively collect leaf items with their paths
|
// Recursively collect leaf items with their paths
|
||||||
void collectLeafPaths(OrderLineItem item, List<String> currentPath) {
|
void collectLeafPaths(OrderLineItem item, List<String> currentPath) {
|
||||||
|
final menuItem = _menuItemsById[item.itemId];
|
||||||
|
|
||||||
|
// Skip default items - they don't need to be repeated in the cart
|
||||||
|
if (menuItem?.isCheckedByDefault == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final children = _cart!.lineItems
|
final children = _cart!.lineItems
|
||||||
.where((child) =>
|
.where((child) =>
|
||||||
child.parentOrderLineItemId == item.orderLineItemId &&
|
child.parentOrderLineItemId == item.orderLineItemId &&
|
||||||
!child.isDeleted)
|
!child.isDeleted)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final menuItem = _menuItemsById[item.itemId];
|
|
||||||
final itemName = menuItem?.name ?? "Item #${item.itemId}";
|
final itemName = menuItem?.name ?? "Item #${item.itemId}";
|
||||||
|
|
||||||
if (children.isEmpty) {
|
if (children.isEmpty) {
|
||||||
|
|
@ -486,6 +520,21 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// Sales tax
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Tax (8.25%)",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"\$${_cart!.tax.toStringAsFixed(2)}",
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
// Only show delivery fee for delivery orders (OrderTypeID = 3)
|
// Only show delivery fee for delivery orders (OrderTypeID = 3)
|
||||||
if (_cart!.deliveryFee > 0 && _cart!.orderTypeId == 3) ...[
|
if (_cart!.deliveryFee > 0 && _cart!.orderTypeId == 3) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,40 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
_itemsByCategory.clear();
|
_itemsByCategory.clear();
|
||||||
_itemsByParent.clear();
|
_itemsByParent.clear();
|
||||||
|
|
||||||
|
print('[MenuBrowse] _organizeItems: ${_allItems.length} total items');
|
||||||
|
|
||||||
|
// First pass: identify category items (root items where itemId == categoryId)
|
||||||
|
// These are the category headers themselves, NOT menu items
|
||||||
|
final categoryItemIds = <int>{};
|
||||||
|
for (final item in _allItems) {
|
||||||
|
if (item.isRootItem && item.itemId == item.categoryId) {
|
||||||
|
categoryItemIds.add(item.itemId);
|
||||||
|
// Just register the category key (empty list for now)
|
||||||
|
_itemsByCategory.putIfAbsent(item.itemId, () => []);
|
||||||
|
print('[MenuBrowse] Category found: ${item.name} (ID=${item.itemId})');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('[MenuBrowse] Found ${categoryItemIds.length} categories: $categoryItemIds');
|
||||||
|
|
||||||
|
// Second pass: organize menu items and modifiers
|
||||||
for (final item in _allItems) {
|
for (final item in _allItems) {
|
||||||
// Skip inactive items
|
// Skip inactive items
|
||||||
if (!item.isActive) continue;
|
if (!item.isActive) continue;
|
||||||
|
|
||||||
if (item.isRootItem) {
|
// Skip category header items (they're not menu items to display)
|
||||||
_itemsByCategory.putIfAbsent(item.categoryId, () => []).add(item);
|
if (categoryItemIds.contains(item.itemId)) continue;
|
||||||
|
|
||||||
|
// Check if parent is a category
|
||||||
|
if (categoryItemIds.contains(item.parentItemId)) {
|
||||||
|
// Direct child of a category = menu item (goes in _itemsByCategory)
|
||||||
|
_itemsByCategory.putIfAbsent(item.parentItemId, () => []).add(item);
|
||||||
|
print('[MenuBrowse] Menu item: ${item.name} -> category ${item.parentItemId}');
|
||||||
} else {
|
} else {
|
||||||
// Prevent an item from being its own child
|
// Child of a menu item = modifier (goes in _itemsByParent)
|
||||||
if (item.itemId != item.parentItemId) {
|
if (item.itemId != item.parentItemId) {
|
||||||
_itemsByParent.putIfAbsent(item.parentItemId, () => []).add(item);
|
_itemsByParent.putIfAbsent(item.parentItemId, () => []).add(item);
|
||||||
|
print('[MenuBrowse] Modifier: ${item.name} -> parent ${item.parentItemId}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +127,11 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
for (final list in _itemsByParent.values) {
|
for (final list in _itemsByParent.values) {
|
||||||
list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: print final counts
|
||||||
|
for (final entry in _itemsByCategory.entries) {
|
||||||
|
print('[MenuBrowse] Category ${entry.key}: ${entry.value.length} items');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> _getUniqueCategoryIds() {
|
List<int> _getUniqueCategoryIds() {
|
||||||
|
|
@ -386,6 +415,56 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds category background - tries image first, falls back to styled text
|
||||||
|
Widget _buildCategoryBackground(int categoryId, String categoryName) {
|
||||||
|
return Image.network(
|
||||||
|
"$_imageBaseUrl/categories/$categoryId.png",
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
semanticLabel: categoryName,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Image.network(
|
||||||
|
"$_imageBaseUrl/categories/$categoryId.jpg",
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
semanticLabel: categoryName,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
// No image - show large styled category name
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
Theme.of(context).colorScheme.tertiary,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Text(
|
||||||
|
categoryName,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(2, 2),
|
||||||
|
blurRadius: 4,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildCategoryHeader(int categoryId, String categoryName) {
|
Widget _buildCategoryHeader(int categoryId, String categoryName) {
|
||||||
final isExpanded = _expandedCategoryId == categoryId;
|
final isExpanded = _expandedCategoryId == categoryId;
|
||||||
|
|
||||||
|
|
@ -408,33 +487,8 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
// Category image background
|
// Category image background or styled text fallback
|
||||||
Image.network(
|
_buildCategoryBackground(categoryId, categoryName),
|
||||||
"$_imageBaseUrl/categories/$categoryId.png",
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
semanticLabel: categoryName,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Image.network(
|
|
||||||
"$_imageBaseUrl/categories/$categoryId.jpg",
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
semanticLabel: categoryName,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
// No image - show category name as fallback
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
categoryName,
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Top edge gradient
|
// Top edge gradient
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
@ -473,27 +527,6 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Expand/collapse indicator
|
|
||||||
Positioned(
|
|
||||||
right: 16,
|
|
||||||
bottom: 12,
|
|
||||||
child: AnimatedRotation(
|
|
||||||
turns: isExpanded ? 0.5 : 0,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withAlpha(100),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.keyboard_arrow_down,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -836,18 +869,17 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> {
|
||||||
_initializeDefaults(widget.item.itemId);
|
_initializeDefaults(widget.item.itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively initialize default selections
|
/// Recursively initialize default selections for the ENTIRE tree
|
||||||
|
/// This ensures defaults are pre-selected even for nested items
|
||||||
void _initializeDefaults(int parentId) {
|
void _initializeDefaults(int parentId) {
|
||||||
final children = widget.itemsByParent[parentId] ?? [];
|
final children = widget.itemsByParent[parentId] ?? [];
|
||||||
print('[Customization] _initializeDefaults for parentId=$parentId, found ${children.length} children');
|
|
||||||
for (final child in children) {
|
for (final child in children) {
|
||||||
print('[Customization] Child ${child.name} (ID=${child.itemId}): isCheckedByDefault=${child.isCheckedByDefault}');
|
|
||||||
if (child.isCheckedByDefault) {
|
if (child.isCheckedByDefault) {
|
||||||
_selectedItemIds.add(child.itemId);
|
_selectedItemIds.add(child.itemId);
|
||||||
_defaultItemIds.add(child.itemId); // Remember this was a default
|
_defaultItemIds.add(child.itemId);
|
||||||
print('[Customization] -> Added to defaults and selected');
|
|
||||||
_initializeDefaults(child.itemId);
|
|
||||||
}
|
}
|
||||||
|
// Always recurse into all children to find nested defaults
|
||||||
|
_initializeDefaults(child.itemId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -891,10 +923,22 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively validate selected children
|
// Recursively validate ALL children that are modifier groups (have their own children)
|
||||||
for (final child in selectedChildren) {
|
// This ensures we check required selections in nested modifier groups
|
||||||
if (!validateRecursive(child.itemId, child)) {
|
for (final child in children) {
|
||||||
return false;
|
final hasGrandchildren = widget.itemsByParent.containsKey(child.itemId) &&
|
||||||
|
(widget.itemsByParent[child.itemId]?.isNotEmpty ?? false);
|
||||||
|
|
||||||
|
if (hasGrandchildren) {
|
||||||
|
// This is a modifier group - always validate it
|
||||||
|
if (!validateRecursive(child.itemId, child)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (_selectedItemIds.contains(child.itemId)) {
|
||||||
|
// This is a leaf option that's selected - validate its children (if any)
|
||||||
|
if (!validateRecursive(child.itemId, child)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
import "dart:math";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:provider/provider.dart";
|
import "package:provider/provider.dart";
|
||||||
|
import "package:dchs_flutter_beacon/dchs_flutter_beacon.dart";
|
||||||
|
|
||||||
import "../app/app_router.dart";
|
import "../app/app_router.dart";
|
||||||
import "../app/app_state.dart";
|
import "../app/app_state.dart";
|
||||||
import "../services/api.dart";
|
import "../services/api.dart";
|
||||||
import "../services/auth_storage.dart";
|
import "../services/auth_storage.dart";
|
||||||
|
import "../services/beacon_permissions.dart";
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
@ -14,60 +17,397 @@ class SplashScreen extends StatefulWidget {
|
||||||
State<SplashScreen> createState() => _SplashScreenState();
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SplashScreenState extends State<SplashScreen> {
|
class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMixin {
|
||||||
Timer? _timer;
|
// Bouncing logo animation
|
||||||
|
late AnimationController _bounceController;
|
||||||
|
double _x = 100;
|
||||||
|
double _y = 100;
|
||||||
|
double _dx = 2.5;
|
||||||
|
double _dy = 2.0;
|
||||||
|
Color _logoColor = Colors.white;
|
||||||
|
final Random _random = Random();
|
||||||
|
|
||||||
|
// Rotating status text
|
||||||
|
Timer? _statusTimer;
|
||||||
|
int _statusIndex = 0;
|
||||||
|
static const List<String> _statusPhrases = [
|
||||||
|
"scanning...",
|
||||||
|
"listening...",
|
||||||
|
"searching...",
|
||||||
|
"locating...",
|
||||||
|
"analyzing...",
|
||||||
|
"connecting...",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Beacon scanning state
|
||||||
|
Map<String, int> _uuidToBeaconId = {};
|
||||||
|
final Map<String, List<int>> _beaconRssiSamples = {};
|
||||||
|
final Map<String, int> _beaconDetectionCount = {};
|
||||||
|
bool _scanComplete = false;
|
||||||
|
BeaconResult? _bestBeacon;
|
||||||
|
|
||||||
|
static const List<Color> _colors = [
|
||||||
|
Colors.white,
|
||||||
|
Colors.red,
|
||||||
|
Colors.green,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.yellow,
|
||||||
|
Colors.purple,
|
||||||
|
Colors.cyan,
|
||||||
|
Colors.orange,
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
print('[Splash] 🚀 SplashScreen initState called');
|
print('[Splash] 🚀 Starting with bouncing logo + beacon scan');
|
||||||
|
|
||||||
_timer = Timer(const Duration(milliseconds: 2400), () async {
|
// Start bouncing animation
|
||||||
print('[Splash] ⏰ Timer fired, starting navigation logic');
|
_bounceController = AnimationController(
|
||||||
if (!mounted) return;
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 16), // ~60fps
|
||||||
|
)..addListener(_updatePosition)..repeat();
|
||||||
|
|
||||||
// Check for saved authentication credentials
|
// Start rotating status text (randomized)
|
||||||
print('[Splash] 🔐 Checking for saved auth credentials...');
|
_statusTimer = Timer.periodic(const Duration(milliseconds: 1600), (_) {
|
||||||
final credentials = await AuthStorage.loadAuth();
|
if (mounted) {
|
||||||
if (credentials != null) {
|
setState(() {
|
||||||
print('[Splash] ✅ Found saved credentials: UserID=${credentials.userId}');
|
int newIndex;
|
||||||
// Restore authentication state
|
do {
|
||||||
Api.setAuthToken(credentials.token);
|
newIndex = _random.nextInt(_statusPhrases.length);
|
||||||
final appState = context.read<AppState>();
|
} while (newIndex == _statusIndex && _statusPhrases.length > 1);
|
||||||
appState.setUserId(credentials.userId);
|
_statusIndex = newIndex;
|
||||||
} else {
|
});
|
||||||
print('[Splash] ℹ️ No saved credentials found');
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the initialization flow
|
||||||
|
_initializeApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updatePosition() {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
const logoWidth = 180.0;
|
||||||
|
const logoHeight = 60.0;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_x += _dx;
|
||||||
|
_y += _dy;
|
||||||
|
|
||||||
|
// Bounce off edges and change color
|
||||||
|
if (_x <= 0 || _x >= size.width - logoWidth) {
|
||||||
|
_dx = -_dx;
|
||||||
|
_changeColor();
|
||||||
|
}
|
||||||
|
if (_y <= 0 || _y >= size.height - logoHeight) {
|
||||||
|
_dy = -_dy;
|
||||||
|
_changeColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
// Keep in bounds
|
||||||
|
_x = _x.clamp(0, size.width - logoWidth);
|
||||||
// Always go to beacon scan first - allows browsing without login
|
_y = _y.clamp(0, size.height - logoHeight);
|
||||||
print('[Splash] 📡 Navigating to beacon scan screen');
|
|
||||||
Navigator.of(context).pushReplacementNamed(AppRoutes.beaconScan);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _changeColor() {
|
||||||
|
final newColor = _colors[_random.nextInt(_colors.length)];
|
||||||
|
if (newColor != _logoColor) {
|
||||||
|
_logoColor = newColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeApp() async {
|
||||||
|
// Check for saved auth credentials
|
||||||
|
print('[Splash] 🔐 Checking for saved auth credentials...');
|
||||||
|
final credentials = await AuthStorage.loadAuth();
|
||||||
|
if (credentials != null && mounted) {
|
||||||
|
print('[Splash] ✅ Found saved credentials: UserID=${credentials.userId}');
|
||||||
|
Api.setAuthToken(credentials.token);
|
||||||
|
final appState = context.read<AppState>();
|
||||||
|
appState.setUserId(credentials.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start beacon scanning in background
|
||||||
|
await _performBeaconScan();
|
||||||
|
|
||||||
|
// Navigate based on results
|
||||||
|
if (!mounted) return;
|
||||||
|
_navigateToNextScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _performBeaconScan() async {
|
||||||
|
print('[Splash] 📡 Starting beacon scan...');
|
||||||
|
|
||||||
|
// Request permissions
|
||||||
|
final granted = await BeaconPermissions.requestPermissions();
|
||||||
|
if (!granted) {
|
||||||
|
print('[Splash] ❌ Permissions denied');
|
||||||
|
_scanComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch beacon list from server
|
||||||
|
try {
|
||||||
|
_uuidToBeaconId = await Api.listAllBeacons();
|
||||||
|
print('[Splash] Loaded ${_uuidToBeaconId.length} beacons from database');
|
||||||
|
} catch (e) {
|
||||||
|
print('[Splash] Error loading beacons: $e');
|
||||||
|
_scanComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uuidToBeaconId.isEmpty) {
|
||||||
|
print('[Splash] No beacons in database');
|
||||||
|
_scanComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize beacon scanning
|
||||||
|
try {
|
||||||
|
await flutterBeacon.initializeScanning;
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// Create regions for all known UUIDs
|
||||||
|
final regions = _uuidToBeaconId.keys.map((uuid) {
|
||||||
|
final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}';
|
||||||
|
return Region(identifier: uuid, proximityUUID: formattedUUID);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Perform scan cycles
|
||||||
|
for (int scanCycle = 1; scanCycle <= 5; scanCycle++) {
|
||||||
|
print('[Splash] ----- Scan cycle $scanCycle/5 -----');
|
||||||
|
|
||||||
|
StreamSubscription<RangingResult>? subscription;
|
||||||
|
subscription = flutterBeacon.ranging(regions).listen((result) {
|
||||||
|
for (var beacon in result.beacons) {
|
||||||
|
final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', '');
|
||||||
|
final rssi = beacon.rssi;
|
||||||
|
|
||||||
|
if (_uuidToBeaconId.containsKey(uuid)) {
|
||||||
|
_beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi);
|
||||||
|
_beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1;
|
||||||
|
print('[Splash] ✅ Beacon ${_uuidToBeaconId[uuid]}, RSSI=$rssi');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
await subscription.cancel();
|
||||||
|
|
||||||
|
// Check for early exit after 3 cycles
|
||||||
|
if (scanCycle >= 3 && _beaconRssiSamples.isNotEmpty && _canExitEarly()) {
|
||||||
|
print('[Splash] ⚡ Early exit - stable readings');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanCycle < 5) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find best beacon
|
||||||
|
_bestBeacon = _findBestBeacon();
|
||||||
|
print('[Splash] 🎯 Best beacon: ${_bestBeacon?.beaconId ?? "none"}');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
print('[Splash] Scan error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
_scanComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canExitEarly() {
|
||||||
|
if (_beaconRssiSamples.isEmpty) return false;
|
||||||
|
|
||||||
|
bool hasEnoughSamples = _beaconRssiSamples.values.any((s) => s.length >= 3);
|
||||||
|
if (!hasEnoughSamples) return false;
|
||||||
|
|
||||||
|
for (final entry in _beaconRssiSamples.entries) {
|
||||||
|
final samples = entry.value;
|
||||||
|
if (samples.length < 3) continue;
|
||||||
|
|
||||||
|
final avg = samples.reduce((a, b) => a + b) / samples.length;
|
||||||
|
final variance = samples.map((r) => (r - avg) * (r - avg)).reduce((a, b) => a + b) / samples.length;
|
||||||
|
|
||||||
|
if (variance > 50) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_beaconRssiSamples.length > 1) {
|
||||||
|
final avgRssis = <String, double>{};
|
||||||
|
for (final entry in _beaconRssiSamples.entries) {
|
||||||
|
if (entry.value.isNotEmpty) {
|
||||||
|
avgRssis[entry.key] = entry.value.reduce((a, b) => a + b) / entry.value.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final sorted = avgRssis.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
if (sorted.length >= 2 && (sorted[0].value - sorted[1].value) < 5) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BeaconResult? _findBestBeacon() {
|
||||||
|
if (_beaconRssiSamples.isEmpty) return null;
|
||||||
|
|
||||||
|
String? bestUuid;
|
||||||
|
double bestAvgRssi = -999;
|
||||||
|
|
||||||
|
for (final entry in _beaconRssiSamples.entries) {
|
||||||
|
final samples = entry.value;
|
||||||
|
final detections = _beaconDetectionCount[entry.key] ?? 0;
|
||||||
|
|
||||||
|
if (detections < 3) continue;
|
||||||
|
|
||||||
|
final avgRssi = samples.reduce((a, b) => a + b) / samples.length;
|
||||||
|
if (avgRssi > bestAvgRssi && avgRssi >= -85) {
|
||||||
|
bestAvgRssi = avgRssi;
|
||||||
|
bestUuid = entry.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestUuid != null) {
|
||||||
|
return BeaconResult(
|
||||||
|
uuid: bestUuid,
|
||||||
|
beaconId: _uuidToBeaconId[bestUuid]!,
|
||||||
|
avgRssi: bestAvgRssi,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to strongest signal even if doesn't meet threshold
|
||||||
|
if (_beaconRssiSamples.isNotEmpty) {
|
||||||
|
for (final entry in _beaconRssiSamples.entries) {
|
||||||
|
final samples = entry.value;
|
||||||
|
if (samples.isEmpty) continue;
|
||||||
|
final avgRssi = samples.reduce((a, b) => a + b) / samples.length;
|
||||||
|
if (avgRssi > bestAvgRssi) {
|
||||||
|
bestAvgRssi = avgRssi;
|
||||||
|
bestUuid = entry.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestUuid != null) {
|
||||||
|
return BeaconResult(
|
||||||
|
uuid: bestUuid,
|
||||||
|
beaconId: _uuidToBeaconId[bestUuid]!,
|
||||||
|
avgRssi: bestAvgRssi,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _navigateToNextScreen() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (_bestBeacon != null) {
|
||||||
|
// Auto-select business from beacon
|
||||||
|
try {
|
||||||
|
final mapping = await Api.getBusinessFromBeacon(beaconId: _bestBeacon!.beaconId);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final appState = context.read<AppState>();
|
||||||
|
appState.setBusinessAndServicePoint(
|
||||||
|
mapping.businessId,
|
||||||
|
mapping.servicePointId,
|
||||||
|
businessName: mapping.businessName,
|
||||||
|
servicePointName: mapping.servicePointName,
|
||||||
|
);
|
||||||
|
Api.setBusinessId(mapping.businessId);
|
||||||
|
|
||||||
|
print('[Splash] 🎉 Auto-selected: ${mapping.businessName}');
|
||||||
|
|
||||||
|
Navigator.of(context).pushReplacementNamed(
|
||||||
|
AppRoutes.menuBrowse,
|
||||||
|
arguments: {
|
||||||
|
'businessId': mapping.businessId,
|
||||||
|
'servicePointId': mapping.servicePointId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
print('[Splash] Error mapping beacon to business: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No beacon or error - go to restaurant select
|
||||||
|
print('[Splash] Going to restaurant select');
|
||||||
|
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_bounceController.dispose();
|
||||||
|
_statusTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Center(
|
body: Stack(
|
||||||
child: Text(
|
children: [
|
||||||
"PAYFRIT",
|
// Centered static status text
|
||||||
style: TextStyle(
|
Center(
|
||||||
color: Colors.white,
|
child: Column(
|
||||||
fontSize: 38,
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontWeight: FontWeight.w800,
|
children: [
|
||||||
letterSpacing: 3,
|
const Text(
|
||||||
|
"site survey",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
_statusPhrases[_statusIndex],
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
// Bouncing logo
|
||||||
|
Positioned(
|
||||||
|
left: _x,
|
||||||
|
top: _y,
|
||||||
|
child: Text(
|
||||||
|
"PAYFRIT",
|
||||||
|
style: TextStyle(
|
||||||
|
color: _logoColor,
|
||||||
|
fontSize: 38,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BeaconResult {
|
||||||
|
final String uuid;
|
||||||
|
final int beaconId;
|
||||||
|
final double avgRssi;
|
||||||
|
|
||||||
|
const BeaconResult({
|
||||||
|
required this.uuid,
|
||||||
|
required this.beaconId,
|
||||||
|
required this.avgRssi,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
96
pubspec.lock
|
|
@ -1,6 +1,22 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -25,6 +41,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -41,6 +73,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
dchs_flutter_beacon:
|
dchs_flutter_beacon:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -78,6 +118,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|
@ -112,6 +160,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.7.2"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.9.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -256,6 +320,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
version: "0.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -272,6 +344,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -429,6 +509,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
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:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.35.0"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^4.0.0
|
flutter_lints: ^4.0.0
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "icon.png"
|
||||||
|
adaptive_icon_background: "#000000"
|
||||||
|
adaptive_icon_foreground: "icon.png"
|
||||||
|
remove_alpha_ios: true
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
|
||||||