diff --git a/flutter_app/lib/feedback_helper.dart b/flutter_app/lib/feedback_helper.dart index e9aca38..b963f0e 100644 --- a/flutter_app/lib/feedback_helper.dart +++ b/flutter_app/lib/feedback_helper.dart @@ -5,6 +5,10 @@ import 'package:flutter/foundation.dart' show kIsWeb, debugPrint; import 'dart:io' show Platform; class AppFeedback { + // Global settings for sound and haptic vibration feedback + static bool isSoundEnabled = true; + static bool isHapticEnabled = true; + static final Map _players = { 'click.mp3': AudioPlayer(), 'select.mp3': AudioPlayer(), @@ -30,6 +34,7 @@ class AppFeedback { /// Play a sound from assets/sounds/ with zero delay static Future playSound(String fileName) async { + if (!isSoundEnabled) return; try { if (!_initialized) { init(); @@ -54,7 +59,7 @@ class AppFeedback { /// Trigger native vibration for Android with customized duration (in milliseconds) static void _triggerVibration(int duration) async { - if (kIsWeb) return; + if (!isHapticEnabled || kIsWeb) return; try { if (_isAndroid) { if (await Vibration.hasVibrator() == true) { @@ -68,57 +73,70 @@ class AppFeedback { /// Light haptic feedback + subtle click sound static void click() { - HapticFeedback.lightImpact(); - _triggerVibration(30); + if (isHapticEnabled) { + HapticFeedback.lightImpact(); + _triggerVibration(30); + } playSound('click.mp3'); } /// Medium haptic + selection sound static void select() { - HapticFeedback.mediumImpact(); - _triggerVibration(60); + if (isHapticEnabled) { + HapticFeedback.mediumImpact(); + _triggerVibration(60); + } playSound('select.mp3'); } /// Celebration sound + strong haptic vibration static void success() { - HapticFeedback.heavyImpact(); - _doubleVibrate(); + if (isHapticEnabled) { + HapticFeedback.heavyImpact(); + _doubleVibrate(); + } playSound('success.mp3'); } /// Wrong input -> error haptic (double vibrate) + error sound static void error() { - _doubleVibrate(); + if (isHapticEnabled) { + _doubleVibrate(); + } playSound('error.mp3'); } /// Light haptic feedback (e.g. chip selections) static void lightHaptic() { + if (!isHapticEnabled) return; HapticFeedback.lightImpact(); _triggerVibration(30); } /// Medium haptic feedback static void mediumHaptic() { + if (!isHapticEnabled) return; HapticFeedback.mediumImpact(); _triggerVibration(60); } /// Heavy haptic feedback static void heavyHaptic() { + if (!isHapticEnabled) return; HapticFeedback.heavyImpact(); _triggerVibration(100); } /// Subtle haptic feedback (e.g. screen navigation) static void subtleHaptic() { + if (!isHapticEnabled) return; HapticFeedback.selectionClick(); _triggerVibration(20); } /// Double vibrate pattern using vibration package static Future _doubleVibrate() async { + if (!isHapticEnabled) return; try { if (!kIsWeb && await Vibration.hasVibrator() == true) { // Double vibrate pattern: wait 0, vibrate 100ms, wait 100ms, vibrate 100ms diff --git a/flutter_app/lib/screens/welcome_screen.dart b/flutter_app/lib/screens/welcome_screen.dart index 123b98c..3e92c8b 100644 --- a/flutter_app/lib/screens/welcome_screen.dart +++ b/flutter_app/lib/screens/welcome_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:mobile_number/mobile_number.dart'; +import 'dart:math' as math; import '../theme.dart'; import '../route_transitions.dart'; import '../feedback_helper.dart'; @@ -180,23 +181,31 @@ class _WelcomeScreenState extends State with SingleTickerProvider child: SafeArea( child: Column( children: [ - // Top Progress bar indicator - 1st segment active + // Top Progress bar indicator - 1st segment active + Quick Feedback Settings Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( - children: List.generate(5, (index) { - return Expanded( - child: AnimatedContainer( - duration: const Duration(milliseconds: 350), - height: 4, - margin: const EdgeInsets.symmetric(horizontal: 2.0), - decoration: BoxDecoration( - color: index == 0 ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(2), - ), + children: [ + Expanded( + child: Row( + children: List.generate(5, (index) { + return Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 350), + height: 4, + margin: const EdgeInsets.symmetric(horizontal: 2.0), + decoration: BoxDecoration( + color: index == 0 ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + }), ), - ); - }), + ), + const SizedBox(width: 12), + _buildSettingsButton(theme), + ], ), ), @@ -367,156 +376,8 @@ class _WelcomeScreenState extends State with SingleTickerProvider } Widget _buildMockDashboard(ThemeData theme) { - return Container( - width: 280, - height: 230, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: theme.colorScheme.outlineVariant), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 20, - offset: const Offset(0, 8), - ) - ], - ), - child: Stack( - clipBehavior: Clip.none, - children: [ - Column( - children: [ - Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Colors.grey[50], - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - border: Border(bottom: BorderSide(color: Colors.grey[200]!)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 8), - Container( - width: 70, - height: 8, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(4), - ), - ), - ], - ), - Container( - width: 14, - height: 14, - decoration: BoxDecoration( - color: Colors.grey[300], - shape: BoxShape.circle, - ), - ) - ], - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildMockRow(Icons.description, Colors.red[50]!, Colors.red), - const SizedBox(height: 14), - _buildMockRow(Icons.poll, Colors.green[50]!, Colors.green), - ], - ), - ), - ) - ], - ), - Positioned( - top: 60, - right: -12, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.grey[200]!), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 10, - offset: const Offset(0, 4), - ) - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(2), - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.check, - color: Colors.white, - size: 10, - ), - ), - const SizedBox(width: 6), - const Text( - 'Saved', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 11, - color: Colors.black, - ), - ) - ], - ), - ), - ), - Positioned( - bottom: 30, - left: -16, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: theme.colorScheme.primary.withOpacity(0.2), - blurRadius: 12, - offset: const Offset(0, 6), - ) - ], - ), - child: const Text( - '₹ 4,500', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ) - ], - ), + return const Center( + child: RetailUniverseVisual(), ); } @@ -584,52 +445,403 @@ class _WelcomeScreenState extends State with SingleTickerProvider ); } - Widget _buildMockRow(IconData icon, Color bg, Color color) { - return Container( - height: 48, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: bg.withOpacity(0.4), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[100]!), + Widget _buildSettingsButton(ThemeData theme) { + final hasSound = AppFeedback.isSoundEnabled; + final hasHaptic = AppFeedback.isHapticEnabled; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _showFeedbackSettings(theme), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + hasSound ? Icons.volume_up_rounded : Icons.volume_off_rounded, + size: 16, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 6), + Icon( + hasHaptic ? Icons.vibration_rounded : Icons.phone_android_rounded, + size: 16, + color: theme.colorScheme.primary, + ), + ], + ), + ), ), - child: Row( + ); + } + + void _showFeedbackSettings(ThemeData theme) { + AppFeedback.click(); + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Feedback Settings', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + AppFeedback.click(); + Navigator.pop(context); + }, + ), + ], + ), + const SizedBox(height: 16), + ListTile( + leading: Icon( + AppFeedback.isSoundEnabled ? Icons.volume_up : Icons.volume_off, + color: theme.colorScheme.primary, + ), + title: const Text('Sound Effects', style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: const Text('Play subtle sounds on button clicks & selections'), + trailing: Switch( + value: AppFeedback.isSoundEnabled, + activeColor: theme.colorScheme.primary, + onChanged: (val) { + AppFeedback.isSoundEnabled = val; + AppFeedback.click(); + setModalState(() {}); + setState(() {}); + }, + ), + ), + const Divider(), + ListTile( + leading: Icon( + AppFeedback.isHapticEnabled ? Icons.vibration : Icons.mobile_off, + color: theme.colorScheme.primary, + ), + title: const Text('Haptic Vibration', style: TextStyle(fontWeight: FontWeight.bold)), + subtitle: const Text('Feel tactical vibrations on user interaction'), + trailing: Switch( + value: AppFeedback.isHapticEnabled, + activeColor: theme.colorScheme.primary, + onChanged: (val) { + AppFeedback.isHapticEnabled = val; + AppFeedback.click(); + setModalState(() {}); + setState(() {}); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + }, + ); + }, + ); + } +} + +class RetailUniverseVisual extends StatefulWidget { + const RetailUniverseVisual({super.key}); + + @override + State createState() => _RetailUniverseVisualState(); +} + +class _RetailUniverseVisualState extends State with TickerProviderStateMixin { + late AnimationController _orbitController; + late AnimationController _pulseController; + int _activeIndex = 0; + + final List> _retailUniverse = [ + { + 'title': 'Grocery & Retail', + 'subtitle': 'Groceries, Wholesale, Dept. Stores, Xerox, Stationery', + 'icon': Icons.shopping_basket_rounded, + 'color': Colors.green, + 'bgColor': const Color(0xFFE8F5E9), + }, + { + 'title': 'Food & Beverages', + 'subtitle': 'Restaurants, Bakeries, Dairy, Ice Cream, Coffee Shops', + 'icon': Icons.restaurant_rounded, + 'color': Colors.orange, + 'bgColor': const Color(0xFFFFF3E0), + }, + { + 'title': 'Wellness & Beauty', + 'subtitle': 'Salon, Spa, Beauty Parlours, Cosmetics, Fitness', + 'icon': Icons.spa_rounded, + 'color': Colors.pink, + 'bgColor': const Color(0xFFFCE4EC), + }, + { + 'title': 'Fashion & Lifestyle', + 'subtitle': 'Fashion & Jewellery Boutique, Lifestyle, Footwear Stores', + 'icon': Icons.checkroom_rounded, + 'color': Colors.indigo, + 'bgColor': const Color(0xFFE8EAF6), + }, + { + 'title': 'Electronics & Tech', + 'subtitle': 'Electricals, Computers, Mobiles, Appliances', + 'icon': Icons.computer_rounded, + 'color': Colors.blue, + 'bgColor': const Color(0xFFE3F2FD), + }, + { + 'title': 'Logistics & Healthcare', + 'subtitle': 'PozoHealth, Smart Parking, Boating, Transport & Logistics', + 'icon': Icons.local_hospital_rounded, + 'color': Colors.teal, + 'bgColor': const Color(0xFFE0F2F1), + }, + ]; + + @override + void initState() { + super.initState(); + _orbitController = AnimationController( + vsync: this, + duration: const Duration(seconds: 30), + )..repeat(); + + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + _startAutoRotation(); + } + + void _startAutoRotation() { + Future.delayed(const Duration(milliseconds: 3500), _nextCategory); + } + + void _nextCategory() { + if (!mounted) return; + setState(() { + _activeIndex = (_activeIndex + 1) % _retailUniverse.length; + }); + _startAutoRotation(); + } + + void _setActiveCategory(int index) { + AppFeedback.select(); + setState(() { + _activeIndex = index; + }); + } + + @override + void dispose() { + _orbitController.dispose(); + _pulseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final activeData = _retailUniverse[_activeIndex]; + + return Container( + width: 320, + height: 320, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, children: [ + // 1. Orbital circular path Container( - padding: const EdgeInsets.all(6), + width: 200, + height: 200, decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: color, size: 16), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 100, - height: 6, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(3), - ), - ), - const SizedBox(height: 6), - Container( - width: 50, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), - ), - ), - ], + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey[200]!, + width: 1.5, + ), + ), + ), + + // 2. Rotating orbital icons + AnimatedBuilder( + animation: _orbitController, + builder: (context, child) { + final double baseAngle = _orbitController.value * 2 * math.pi; + return Stack( + children: List.generate(_retailUniverse.length, (index) { + final double angle = baseAngle + (index * 2 * math.pi / _retailUniverse.length); + const double radius = 100.0; + final double x = radius * math.cos(angle); + final double y = radius * math.sin(angle); + final isCurrent = index == _activeIndex; + final item = _retailUniverse[index]; + + return Positioned( + left: 160.0 + x - 24.0, + top: 160.0 + y - 24.0, + child: InteractiveScale( + onTap: () => _setActiveCategory(index), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isCurrent ? item['bgColor'] : Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: isCurrent ? item['color'] : Colors.grey[300]!, + width: isCurrent ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + item['icon'], + color: isCurrent ? item['color'] : Colors.grey[500], + size: 20, + ), + ), + ), + ); + }), + ); + }, + ), + + // 3. Central active node with pulse animation + ScaleTransition( + scale: Tween(begin: 0.95, end: 1.05).animate( + CurvedAnimation( + parent: _pulseController, + curve: Curves.easeInOut, + ), + ), + child: Container( + width: 110, + height: 110, + decoration: BoxDecoration( + color: activeData['bgColor'], + shape: BoxShape.circle, + border: Border.all( + color: activeData['color'], + width: 3, + ), + boxShadow: [ + BoxShadow( + color: activeData['color'].withOpacity(0.2), + blurRadius: 24, + spreadRadius: 4, + ), + ], + ), + child: Center( + child: Icon( + activeData['icon'], + color: activeData['color'], + size: 38, + ), + ), + ), + ), + + // 4. Floating category text label at the bottom + Positioned( + bottom: -32, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.2), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ), + child: KeyedSubtree( + key: ValueKey(_activeIndex), + child: Container( + width: 280, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey[100]!), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 12, + offset: const Offset(0, 4), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + activeData['title'], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: activeData['color'], + ), + ), + const SizedBox(height: 4), + Text( + activeData['subtitle'], + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: Colors.grey[600], + height: 1.3, + ), + ), + ], + ), + ), + ), ), ), - Icon(Icons.more_vert, color: Colors.grey[400], size: 16) ], ), );