diff --git a/android/gradle.properties b/android/gradle.properties index fbee1d8..d5da727 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,2 +1,6 @@ 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 diff --git a/assets/Snapshot_20260722234539.jpg b/assets/Snapshot_20260722234539.jpg deleted file mode 100644 index 126c971..0000000 Binary files a/assets/Snapshot_20260722234539.jpg and /dev/null differ diff --git a/assets/Snapshot_202608072226134.jpg b/assets/Snapshot_202608072226134.jpg new file mode 100644 index 0000000..79a266e Binary files /dev/null and b/assets/Snapshot_202608072226134.jpg differ diff --git a/assets/logo.png b/assets/logo.png index 79a266e..75466f8 100644 Binary files a/assets/logo.png and b/assets/logo.png differ diff --git a/lib/controllers/lg_controller.dart b/lib/controllers/lg_controller.dart index b5cd44b..14383a5 100644 --- a/lib/controllers/lg_controller.dart +++ b/lib/controllers/lg_controller.dart @@ -6,6 +6,7 @@ import '../data/rice_states.dart'; import '../data/irrigation_data.dart'; import '../models/state_data.dart'; import '../data/crop_cycles.dart'; +import '../data/narration_scripts.dart'; class LGController { LGController({ @@ -495,6 +496,42 @@ class LGController { /// no ScreenOverlay sizing, no aspect-ratio stretching. /// Sends a pre-built dashboard balloon KML to the right slave screen. int _stateYield(StateData s) => s.yield.toInt(); + + String? _irrigationNarrationFor(StateData s) { + final irr = IrrigationData.stateWise[s.name]; + if (irr == null) return null; + return '${s.name} receives ${s.rainfall.toInt()} mm of annual rainfall, ' + 'with ${s.irrigatedPercent}% of rice area irrigated. ' + 'Main sources: tubewells ${irr['tubewell']}%, canals ${irr['canal']}%.'; + } + + String? _narrationFor(String stateName) { + switch (stateName) { + case 'West Bengal': + return NarrationScripts.westBengal; + case 'Uttar Pradesh': + return NarrationScripts.uttarPradesh; + case 'Punjab': + return NarrationScripts.punjab; + case 'Andhra Pradesh': + return NarrationScripts.andhraPradesh; + case 'Tamil Nadu': + return NarrationScripts.tamilNadu; + case 'Odisha': + return NarrationScripts.odisha; + case 'Bihar': + return NarrationScripts.bihar; + case 'Chhattisgarh': + return NarrationScripts.chhattisgarh; + case 'Assam': + return NarrationScripts.assam; + case 'Jharkhand': + return NarrationScripts.jharkhand; + default: + return null; + } + } + Future showDashboardBalloon(String balloonKml) async { if (!isConnected) return; await sendKMLToSlave(lastScreen, balloonKml); @@ -510,6 +547,7 @@ class LGController { totalProduction: RiceStates.totalProduction, totalArea: RiceStates.totalArea, avgYield: RiceStates.averageYield, + narration: NarrationScripts.indiaOverview, ); String balloon = national; @@ -545,6 +583,7 @@ class LGController { other: (irr['other'] as num).toDouble(), lat: s.latitude, lon: s.longitude, + narration: _irrigationNarrationFor(s), ); } else { balloon = kml.buildStateDashboard( @@ -556,6 +595,7 @@ class LGController { irrigatedPercent: s.irrigatedPercent, lat: s.latitude, lon: s.longitude, + narration: _narrationFor(s.name), ); } } @@ -611,6 +651,7 @@ class LGController { irrigatedPercent: s.irrigatedPercent, lat: s.latitude, lon: s.longitude, + narration: _narrationFor(s.name), ); await sendKMLToSlave(lastScreen, balloon); } @@ -629,6 +670,7 @@ class LGController { other: (irr['other'] as num).toDouble(), lat: s.latitude, lon: s.longitude, + narration: _irrigationNarrationFor(s), ); await sendKMLToSlave(lastScreen, balloon); } @@ -641,6 +683,7 @@ class LGController { totalProduction: RiceStates.totalProduction, totalArea: RiceStates.totalArea, avgYield: RiceStates.averageYield, + narration: NarrationScripts.indiaOverview, ); await sendKMLToSlave(lastScreen, balloon); } diff --git a/lib/main.dart b/lib/main.dart index 891f87c..0eee439 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,10 +8,11 @@ import 'controllers/lg_controller.dart'; import 'screens/home_screen.dart'; import 'theme/app_theme.dart'; import 'screens/splash_screen.dart'; +import 'screens/onboarding_screen.dart'; void main() async { debugPrint( - '=== RICE FARM BUILD v6: lg1 + absolute-3D + compound-SSH + connect-purge ===', + '=== RICE FARM BUILD v9: voice toggle + irrigation orbit + dashboard narration + onboarding ===', ); WidgetsFlutterBinding.ensureInitialized(); @@ -53,13 +54,67 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, title: 'LG Controller', theme: AppTheme.theme, - home: SplashScreen( - nextScreen: HomeScreen( - sshController: sshController, - settingsController: settingsController, - lgController: lgController, - ), + home: _StartupGate( + sshController: sshController, + settingsController: settingsController, + lgController: lgController, ), ); } } + +class _StartupGate extends StatefulWidget { + final SSHController sshController; + final SettingsController settingsController; + final LGController lgController; + + const _StartupGate({ + required this.sshController, + required this.settingsController, + required this.lgController, + }); + + @override + State<_StartupGate> createState() => _StartupGateState(); +} + +class _StartupGateState extends State<_StartupGate> { + bool? _seenOnboarding; + + @override + void initState() { + super.initState(); + _check(); + } + + Future _check() async { + // Always show onboarding on every launch (demo mode) + setState(() => _seenOnboarding = false); + } + + @override + Widget build(BuildContext context) { + Widget home = SplashScreen( + nextScreen: HomeScreen( + sshController: widget.sshController, + settingsController: widget.settingsController, + lgController: widget.lgController, + ), + ); + + if (_seenOnboarding == null) { + return const Scaffold( + backgroundColor: AppTheme.bgLight, + body: Center(child: CircularProgressIndicator()), + ); + } + + if (_seenOnboarding == false) { + return OnboardingScreen( + onDone: () => setState(() => _seenOnboarding = true), + ); + } + + return home; + } +} diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart index 3bca942..5ce48ac 100644 --- a/lib/screens/about_screen.dart +++ b/lib/screens/about_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../theme/app_theme.dart'; +import 'onboarding_screen.dart'; class AboutScreen extends StatelessWidget { const AboutScreen({super.key}); @@ -29,6 +30,8 @@ class AboutScreen extends StatelessWidget { _buildDataSources(), const SizedBox(height: 16), _buildTechStack(), + const SizedBox(height: 16), + _buildReplayTourButton(context), const SizedBox(height: 24), _buildFooter(), ], @@ -338,6 +341,42 @@ class AboutScreen extends StatelessWidget { ); } + Widget _buildReplayTourButton(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + OnboardingScreen(onDone: () => Navigator.pop(context)), + ), + ), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: AppTheme.purple.withOpacity(0.1), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppTheme.purple.withOpacity(0.3)), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.replay, color: AppTheme.purple, size: 20), + SizedBox(width: 8), + Text( + 'Replay App Tour', + style: TextStyle( + color: AppTheme.purple, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + Widget _buildFooter() { return const Column( children: [ diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 84aede1..2975777 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -259,6 +259,7 @@ class _HomeScreenState extends ConsumerState { bgColor: AppTheme.blue, onTap: () => _navigateTo(MapsScreen(lgController: widget.lgController)), ), + _MenuItem( icon: Icons.info_outline, title: 'About Project', diff --git a/lib/screens/irrigation_screen.dart b/lib/screens/irrigation_screen.dart index a5750e5..8552f53 100644 --- a/lib/screens/irrigation_screen.dart +++ b/lib/screens/irrigation_screen.dart @@ -6,6 +6,7 @@ import '../data/rice_states.dart'; import '../data/irrigation_data.dart'; import '../data/narration_scripts.dart'; import '../theme/app_theme.dart'; +import '../models/state_data.dart'; class IrrigationScreen extends StatefulWidget { final LGController lgController; @@ -20,6 +21,8 @@ class _IrrigationScreenState extends State { bool _isLoading = false; String? _activeView; String _kmlError = ''; + bool _voiceEnabled = false; + String? _orbitingState; @override void initState() { @@ -78,7 +81,7 @@ class _IrrigationScreenState extends State { if (!mounted) return; await Future.delayed(const Duration(seconds: 3)); if (!mounted) return; - await _tts.speak(NarrationScripts.irrigationOverview); + if (_voiceEnabled) await _tts.speak(NarrationScripts.irrigationOverview); if (!mounted) return; await widget.lgController.showNationalDashboard(); if (!mounted) return; @@ -147,7 +150,7 @@ class _IrrigationScreenState extends State { if (!mounted) return; await Future.delayed(const Duration(seconds: 3)); if (!mounted) return; - if (irrData != null) { + if (irrData != null && _voiceEnabled) { await _tts.speak( '${state.name} receives ${state.rainfall.toInt()} millimeters of annual rainfall. ' '${state.irrigatedPercent} percent of rice area is irrigated. ' @@ -167,10 +170,39 @@ class _IrrigationScreenState extends State { } } + Future _orbitState(StateData state) async { + if (_orbitingState == state.name) { + await widget.lgController.stopOrbit(); + if (mounted) setState(() => _orbitingState = null); + return; + } + if (_orbitingState != null) return; + setState(() => _orbitingState = state.name); + try { + final orbitKml = _kmlBuilder.buildOrbitTourKml( + lat: state.latitude, + lng: state.longitude, + range: 500000, + tilt: 60, + ); + await widget.lgController.startOrbit(orbitKml); + await Future.delayed(const Duration(seconds: 24)); + } finally { + if (mounted && _orbitingState == state.name) { + setState(() => _orbitingState = null); + } + } + } + void _showError(String msg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } + void _toggleVoice() { + setState(() => _voiceEnabled = !_voiceEnabled); + if (!_voiceEnabled) _tts.stop(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -253,6 +285,46 @@ class _IrrigationScreenState extends State { ), ), ), + + const SizedBox(width: 8), + GestureDetector( + onTap: _toggleVoice, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: (_voiceEnabled ? const Color(0xFF42A5F5) : Colors.grey) + .withOpacity(0.12), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: (_voiceEnabled ? const Color(0xFF42A5F5) : Colors.grey) + .withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _voiceEnabled ? Icons.volume_up : Icons.volume_off, + color: _voiceEnabled + ? const Color(0xFF42A5F5) + : Colors.grey, + size: 14, + ), + const SizedBox(width: 5), + Text( + 'Voice', + style: TextStyle( + color: _voiceEnabled + ? const Color(0xFF42A5F5) + : Colors.grey, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), const SizedBox(width: 8), if (_isLoading) const SizedBox( @@ -363,35 +435,33 @@ class _IrrigationScreenState extends State { return Column( children: states.map((state) { final isActive = _activeView == state.name; - return GestureDetector( - onTap: () => _flyToStateIrrigation(state.name), - child: Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isActive ? AppTheme.blue.withOpacity(0.1) : AppTheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( color: isActive - ? AppTheme.blue.withOpacity(0.1) - : AppTheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isActive - ? AppTheme.blue.withOpacity(0.3) - : Colors.grey.shade200, - ), + ? AppTheme.blue.withOpacity(0.3) + : Colors.grey.shade200, ), - child: Row( - children: [ - Expanded( - child: Text( - state.name, - style: const TextStyle( - color: AppTheme.textPrimary, - fontSize: 16, - fontWeight: FontWeight.w600, - ), + ), + child: Row( + children: [ + Expanded( + child: Text( + state.name, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, ), ), - Container( + ), + GestureDetector( + onTap: () => _flyToStateIrrigation(state.name), + child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, @@ -399,40 +469,71 @@ class _IrrigationScreenState extends State { decoration: BoxDecoration( color: AppTheme.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppTheme.blue.withOpacity(0.3)), ), - child: Text( - '${state.rainfall.toInt()} mm', - style: const TextStyle( - color: AppTheme.blue, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: AppTheme.green.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '${state.irrigatedPercent}%', - style: const TextStyle( - color: AppTheme.green, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.send, color: AppTheme.blue, size: 14), + SizedBox(width: 5), + Text( + 'Fly to', + style: TextStyle( + color: AppTheme.blue, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], ), ), - ], - ), + ), + const SizedBox(width: 8), + _buildOrbitPill(state), + ], ), ); }).toList(), ); } + + Widget _buildOrbitPill(StateData state) { + final bool isActive = _orbitingState == state.name; + final bool isDisabled = _orbitingState != null && !isActive; + final Color base = isActive + ? Colors.redAccent + : isDisabled + ? Colors.grey + : const Color(0xFF42A5F5); + return GestureDetector( + onTap: isDisabled ? null : () => _orbitState(state), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: base.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: base.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isActive ? Icons.stop : Icons.threesixty, + color: base, + size: 14, + ), + const SizedBox(width: 4), + Text( + isActive ? 'Stop' : 'Orbit', + style: TextStyle( + color: base, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/screens/onboarding_screen.dart b/lib/screens/onboarding_screen.dart new file mode 100644 index 0000000..b5f8122 --- /dev/null +++ b/lib/screens/onboarding_screen.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../theme/app_theme.dart'; + +class OnboardingScreen extends StatefulWidget { + final VoidCallback onDone; + const OnboardingScreen({super.key, required this.onDone}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + final PageController _controller = PageController(); + int _page = 0; + + static const List<_Slide> _slides = [ + _Slide( + icon: Icons.public, + color: Color(0xFF66BB6A), + title: 'Welcome to Rice Farm Visualization', + body: + 'Explore India\'s rice agriculture on the Liquid Galaxy rig, right from your phone.', + ), + _Slide( + icon: Icons.map_outlined, + color: Color(0xFF66BB6A), + title: 'Major Rice Regions', + body: + 'Fly to top rice-producing states, view colored production maps, and orbit any state in 3D.', + ), + _Slide( + icon: Icons.water_drop_outlined, + color: Color(0xFF42A5F5), + title: 'Irrigation & Rainfall', + body: + 'See rainfall patterns and irrigation sources state by state, with live dashboards on the rig.', + ), + _Slide( + icon: Icons.route, + color: Color(0xFFAB7DF6), + title: 'Guided Tours & Seasons', + body: + 'Take narrated tours across the rice belt and watch the seasonal crop cycle unfold.', + ), + _Slide( + icon: Icons.explore_outlined, + color: Color(0xFF42A5F5), + title: 'Synced Navigation', + body: + 'Pan and zoom a live map, and the Liquid Galaxy follows your view in real time.', + ), + ]; + + Future _finish() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('onboarding_done', true); + widget.onDone(); + } + + @override + Widget build(BuildContext context) { + final isLast = _page == _slides.length - 1; + return Scaffold( + backgroundColor: AppTheme.bgDark, + body: SafeArea( + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(12), + child: TextButton( + onPressed: _finish, + child: const Text( + 'Skip', + style: TextStyle( + color: AppTheme.textSecondary, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + Expanded( + child: PageView.builder( + controller: _controller, + itemCount: _slides.length, + onPageChanged: (i) => setState(() => _page = i), + itemBuilder: (context, i) => _buildSlide(_slides[i]), + ), + ), + _buildDots(), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: GestureDetector( + onTap: () { + if (isLast) { + _finish(); + } else { + _controller.nextPage( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + ); + } + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: const Color(0xFF66BB6A), + borderRadius: BorderRadius.circular(14), + ), + child: Text( + isLast ? 'Get Started' : 'Next', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildSlide(_Slide s) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: s.color.withOpacity(0.12), + borderRadius: BorderRadius.circular(30), + ), + child: Icon(s.icon, color: s.color, size: 60), + ), + const SizedBox(height: 40), + Text( + s.title, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 16), + Text( + s.body, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppTheme.textSecondary, + fontSize: 15, + height: 1.5, + ), + ), + ], + ), + ); + } + + Widget _buildDots() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_slides.length, (i) { + final active = i == _page; + return AnimatedContainer( + duration: const Duration(milliseconds: 250), + margin: const EdgeInsets.symmetric(horizontal: 4), + width: active ? 24 : 8, + height: 8, + decoration: BoxDecoration( + color: active + ? const Color(0xFF66BB6A) + : AppTheme.textSecondary.withOpacity(0.3), + borderRadius: BorderRadius.circular(4), + ), + ); + }), + ); + } +} + +class _Slide { + final IconData icon; + final Color color; + final String title; + final String body; + const _Slide({ + required this.icon, + required this.color, + required this.title, + required this.body, + }); +} diff --git a/lib/screens/regions_screen.dart b/lib/screens/regions_screen.dart index d342178..ecca8c9 100644 --- a/lib/screens/regions_screen.dart +++ b/lib/screens/regions_screen.dart @@ -21,6 +21,7 @@ class _RegionsScreenState extends State { final TtsService _tts = TtsService(); String _kmlError = ''; String? _orbitingState; + bool _voiceEnabled = false; @override void initState() { @@ -106,7 +107,7 @@ class _RegionsScreenState extends State { if (!mounted) return; final narration = _getNarration(state.name); if (narration != null) { - await _tts.speak(narration); + if (_voiceEnabled) await _tts.speak(narration); if (!mounted) return; } final safeName = state.name.toLowerCase().replaceAll(' ', '_'); @@ -173,6 +174,11 @@ class _RegionsScreenState extends State { } } + void _toggleVoice() { + setState(() => _voiceEnabled = !_voiceEnabled); + if (!_voiceEnabled) _tts.stop(); + } + void _showError(String msg) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); @@ -229,6 +235,45 @@ class _RegionsScreenState extends State { ), ), const SizedBox(width: 8), + GestureDetector( + onTap: _toggleVoice, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: (_voiceEnabled ? const Color(0xFF66BB6A) : Colors.grey) + .withOpacity(0.12), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: (_voiceEnabled ? const Color(0xFF66BB6A) : Colors.grey) + .withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _voiceEnabled ? Icons.volume_up : Icons.volume_off, + color: _voiceEnabled + ? const Color(0xFF66BB6A) + : Colors.grey, + size: 14, + ), + const SizedBox(width: 5), + Text( + 'Voice', + style: TextStyle( + color: _voiceEnabled + ? const Color(0xFF66BB6A) + : Colors.grey, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), if (_isLoading) const SizedBox( width: 22, diff --git a/lib/services/kml_builder_service.dart b/lib/services/kml_builder_service.dart index dbf3e2c..0a79c4d 100644 --- a/lib/services/kml_builder_service.dart +++ b/lib/services/kml_builder_service.dart @@ -588,6 +588,7 @@ $content required String subtitle, required String cardsRow, String? bottomSection, + String? narration, String headerBg = '#0F766E', String subtitleColor = '#ccfbf1', double lat = 22.0, @@ -597,8 +598,7 @@ $content ? '' : ''' - - $bottomSection + $bottomSection '''; @@ -629,7 +629,13 @@ ${_antiBlink(bId)} - $bottom + $bottom + ${(narration == null || narration.isEmpty) ? '' : ''' + + + 💬 $narration + + '''} ]]> @@ -673,6 +679,7 @@ ${_antiBlink(bId)} required double irrigatedPercent, required double lat, required double lon, + String? narration, }) { final cards = _statCard('${production.toStringAsFixed(1)}M', 'Tonnes', '#22c55e') + @@ -694,6 +701,7 @@ ${_antiBlink(bId)} subtitle: 'Rice Production Profile', cardsRow: cards, bottomSection: bottom, + narration: narration, lat: lat, lon: lon, ); @@ -704,6 +712,7 @@ ${_antiBlink(bId)} required double totalProduction, required double totalArea, required double avgYield, + String? narration, }) { final cards = _statCard( @@ -725,6 +734,7 @@ ${_antiBlink(bId)} title: 'India Rice Production', subtitle: 'National Overview • Top 10 States', cardsRow: cards, + narration: narration, ); } @@ -734,6 +744,7 @@ ${_antiBlink(bId)} required String stageName, required String months, required String description, + String? narration, }) { final cards = _statCard(season, 'Season', '#22c55e') + @@ -752,6 +763,7 @@ ${_antiBlink(bId)} subtitle: '$season Season • $months', cardsRow: cards, bottomSection: bottom, + narration: narration, headerBg: '#166534', subtitleColor: '#bbf7d0', ); @@ -766,6 +778,7 @@ ${_antiBlink(bId)} required double other, required double lat, required double lon, + String? narration, }) { final cards = _statCard('${tubewell.toStringAsFixed(0)}%', 'Tubewell', '#3b82f6') + @@ -790,6 +803,7 @@ ${_antiBlink(bId)} subtitle: 'Irrigation Source Breakdown', cardsRow: cards, bottomSection: bottom, + narration: narration, headerBg: '#0e7490', subtitleColor: '#cffafe', lat: lat, diff --git a/pubspec.lock b/pubspec.lock index 5c06a75..48e3da7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -284,26 +284,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" path: dependency: transitive description: @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.11" typed_data: dependency: transitive description: