From 3b7d953ac413c9e28e6f2b1f776f534b89729939 Mon Sep 17 00:00:00 2001 From: cuntman11 Date: Sun, 20 Jul 2025 13:33:45 +0530 Subject: [PATCH 01/18] add: add mintNFT provider --- lib/pages/counter_page.dart | 38 +++++++++++++ lib/pages/mint_nft/mint_nft_coordinates.dart | 47 +++++++++++++++ lib/providers/counter_provider.dart | 18 ++++++ lib/providers/mint_nft_provider.dart | 60 ++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 lib/pages/counter_page.dart create mode 100644 lib/pages/mint_nft/mint_nft_coordinates.dart create mode 100644 lib/providers/counter_provider.dart create mode 100644 lib/providers/mint_nft_provider.dart diff --git a/lib/pages/counter_page.dart b/lib/pages/counter_page.dart new file mode 100644 index 0000000..52bfda8 --- /dev/null +++ b/lib/pages/counter_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/counter_provider.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; + +class CounterPage extends StatelessWidget { + const CounterPage({super.key}); + + @override + Widget build(BuildContext context) { + return BaseScaffold( + title: "Counter Page", + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "This is the counter page.", + style: TextStyle(fontSize: 30), + ), + const SizedBox(height: 20), + Consumer(builder: (ctx, _, __) { + return Text( + 'Counter: ${Provider.of(ctx, listen: true).getCount()}', + style: const TextStyle(fontSize: 24)); + }), + ElevatedButton( + onPressed: () { + Provider.of(context, listen: false).increment(); + }, + child: const Text("Increment Counter", + style: TextStyle(fontSize: 20, color: Colors.white)), + ) + ], + )), + ); + } +} diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart new file mode 100644 index 0000000..b3e7a13 --- /dev/null +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; + +class MintNftCoordinatesPage extends StatefulWidget { + const MintNftCoordinatesPage({super.key}); + + @override + State createState() => _MintNftCoordinatesPageState(); +} + +class _MintNftCoordinatesPageState extends State { + @override + final latitudeController = TextEditingController(); + final longitudeController = TextEditingController(); + Widget build(BuildContext context) { + return BaseScaffold( + title: "Mint NFT Coordinates", + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children:[ + const Text( + "This is the Mint NFT Coordinates page.", + style: TextStyle(fontSize: 30), + ), + const SizedBox(height: 20), + TextField( + controller: latitudeController, + decoration: const InputDecoration( + labelText: "Latitude", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 10), + TextField( + controller: longitudeController, + decoration: InputDecoration( + labelText: "Longitude", + border: OutlineInputBorder(), + ), + ), + ], + ) + ) + ); + } +} diff --git a/lib/providers/counter_provider.dart b/lib/providers/counter_provider.dart new file mode 100644 index 0000000..9a42ccc --- /dev/null +++ b/lib/providers/counter_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class CounterProvider extends ChangeNotifier { + int _count = 0; + int getCount() => _count; + + void increment() { + _count++; + notifyListeners(); + } + + void decrementCount() { + if (_count > 0) { + _count--; + notifyListeners(); + } + } +} diff --git a/lib/providers/mint_nft_provider.dart b/lib/providers/mint_nft_provider.dart new file mode 100644 index 0000000..30839c7 --- /dev/null +++ b/lib/providers/mint_nft_provider.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +class MintNftProvider extends ChangeNotifier { + double _latitude = 0; + double _longitude = 0; + String _species = ""; + String _imageUri = ""; + String _qrIpfsHash = ""; + String _geoHash = ""; + List _initialPhotos = []; + + double getLatitude() => _latitude; + double getLongitude() => _longitude; + String getSpecies() => _species; + String getImageUri() => _imageUri; + String getQrIpfsHash() => _qrIpfsHash; + String getGeoHash() => _geoHash; + List getInitialPhotos() => _initialPhotos; + + void setLatitude(double latitude) { + _latitude = latitude; + notifyListeners(); + } + void setLongitude(double longitude) { + _longitude = longitude; + notifyListeners(); + } + + void setSpecies(String species) { + _species = species; + notifyListeners(); + } + + void setImageUri(String imageUri) { + _imageUri = imageUri; + notifyListeners(); + } + void setQrIpfsHash(String qrIpfsHash) { + _qrIpfsHash = qrIpfsHash; + notifyListeners(); + } + void setGeoHash(String geoHash) { + _geoHash = geoHash; + notifyListeners(); + } + void setInitialPhotos(List initialPhotos) { + _initialPhotos = initialPhotos; + notifyListeners(); + } + void clearData() { + _latitude = 0; + _longitude = 0; + _species = ""; + _imageUri = ""; + _qrIpfsHash = ""; + _geoHash = ""; + _initialPhotos.clear(); + notifyListeners(); + } +} From 79d05941112f0d0f18bc79631952551eaf605bf4 Mon Sep 17 00:00:00 2001 From: cuntman11 Date: Sun, 20 Jul 2025 18:00:35 +0530 Subject: [PATCH 02/18] add: add tree minting desc page --- lib/main.dart | 28 +++++- lib/pages/mint_nft/mint_nft_coordinates.dart | 58 ++++++++++-- lib/pages/mint_nft/mint_nft_details.dart | 93 +++++++++++++++++++ lib/utils/constants/bottom_nav_constants.dart | 6 ++ lib/utils/constants/route_constants.dart | 5 + 5 files changed, 179 insertions(+), 11 deletions(-) create mode 100644 lib/pages/mint_nft/mint_nft_details.dart diff --git a/lib/main.dart b/lib/main.dart index 00e197a..95bf1db 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,15 @@ -// lib/main.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; + import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; +import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; + import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/providers/theme_provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; + import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -19,7 +23,7 @@ void main() async { } catch (e) { print("No .env file found or error loading it: $e"); } - + runApp(const MyApp()); } @@ -28,7 +32,6 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - // Initialize GoRouter inside MyApp final GoRouter router = GoRouter( initialLocation: RouteConstants.homePath, routes: [ @@ -39,6 +42,21 @@ class MyApp extends StatelessWidget { return const HomePage(); }, ), + GoRoute( + path: RouteConstants.mintNftPath, + name: RouteConstants.mintNft, + builder: (context, state) => const MintNftCoordinatesPage(), + routes: [ + GoRoute( + path: 'details', // This will be /trees/details + name: '${RouteConstants.mintNft}_details', + builder: (BuildContext context, GoRouterState state) { + return const MintNftCoordinatesPage(); + }, + ), + ] + ), + GoRoute( path: RouteConstants.allTreesPath, name: RouteConstants.allTrees, @@ -46,7 +64,6 @@ class MyApp extends StatelessWidget { return const AllTreesPage(); }, routes: [ - // Nested route for tree details GoRoute( path: 'details', // This will be /trees/details name: '${RouteConstants.allTrees}_details', @@ -68,6 +85,7 @@ class MyApp extends StatelessWidget { providers: [ ChangeNotifierProvider(create: (context) => WalletProvider()), ChangeNotifierProvider(create: (context) => ThemeProvider()), + ChangeNotifierProvider(create: (context) => MintNftProvider()), ], child: Consumer( builder: (context, themeProvider, child) { @@ -88,4 +106,4 @@ class MyApp extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index b3e7a13..5319b35 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; class MintNftCoordinatesPage extends StatefulWidget { @@ -9,16 +13,41 @@ class MintNftCoordinatesPage extends StatefulWidget { } class _MintNftCoordinatesPageState extends State { - @override final latitudeController = TextEditingController(); final longitudeController = TextEditingController(); + + void submitCoordinates() { + final latitude = latitudeController.text; + final longitude = longitudeController.text; + + if (latitude.isEmpty || longitude.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Please enter both latitude and longitude.")), + ); + return; + } + Provider.of(context, listen: false) + .setLatitude(double.parse(latitude)); + Provider.of(context, listen: false) + .setLongitude(double.parse(longitude)); + latitudeController.clear(); + longitudeController.clear(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Coordinates submitted successfully.")), + ); + context.push(RouteConstants.mintNftDetailsPath); + } + + @override Widget build(BuildContext context) { return BaseScaffold( - title: "Mint NFT Coordinates", + title: "Mint NFT Coordinates", body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children:[ + children: [ const Text( "This is the Mint NFT Coordinates page.", style: TextStyle(fontSize: 30), @@ -29,19 +58,36 @@ class _MintNftCoordinatesPageState extends State { decoration: const InputDecoration( labelText: "Latitude", border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), ), ), const SizedBox(height: 10), TextField( controller: longitudeController, - decoration: InputDecoration( + decoration: const InputDecoration( labelText: "Longitude", border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), ), ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: submitCoordinates, + child: const Text( + "->", + style: TextStyle(fontSize: 20, color: Colors.white), + ), + ) ], - ) - ) + ), + ), ); } + + @override + void dispose() { + latitudeController.dispose(); + longitudeController.dispose(); + super.dispose(); + } } diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart new file mode 100644 index 0000000..29fa7e0 --- /dev/null +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; + +class MintNftDetailsPage extends StatefulWidget { + const MintNftDetailsPage ({super.key}); + + @override + State createState() => _MintNftCoordinatesPageState(); +} + +class _MintNftCoordinatesPageState extends State { + final latitudeController = TextEditingController(); + final longitudeController = TextEditingController(); + + void submitCoordinates() { + final latitude = latitudeController.text; + final longitude = longitudeController.text; + + if (latitude.isEmpty || longitude.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Please enter both latitude and longitude.")), + ); + return; + } + Provider.of(context, listen: false) + .setLatitude(double.parse(latitude)); + Provider.of(context, listen: false) + .setLongitude(double.parse(longitude)); + latitudeController.clear(); + longitudeController.clear(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Coordinates submitted successfully.")), + ); + context.push(RouteConstants.mintNftDetailsPath); + } + + @override + Widget build(BuildContext context) { + return BaseScaffold( + title: "Mint NFT Coordinates", + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "This is the Mint NFT Coordinates page.", + style: TextStyle(fontSize: 30), + ), + const SizedBox(height: 20), + TextField( + controller: latitudeController, + decoration: const InputDecoration( + labelText: "Latitude", + border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), + ), + ), + const SizedBox(height: 10), + TextField( + controller: longitudeController, + decoration: const InputDecoration( + labelText: "Longitude", + border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: submitCoordinates, + child: const Text( + "->", + style: TextStyle(fontSize: 20, color: Colors.white), + ), + ) + ], + ), + ), + ); + } + + @override + void dispose() { + latitudeController.dispose(); + longitudeController.dispose(); + super.dispose(); + } +} diff --git a/lib/utils/constants/bottom_nav_constants.dart b/lib/utils/constants/bottom_nav_constants.dart index 7012643..a77a094 100644 --- a/lib/utils/constants/bottom_nav_constants.dart +++ b/lib/utils/constants/bottom_nav_constants.dart @@ -29,5 +29,11 @@ class BottomNavConstants { activeIcon: Icons.forest, route: RouteConstants.allTreesPath, ), + BottomNavItem( + label: 'Mint NFT', + icon: Icons.nature_people_outlined, + activeIcon: Icons.nature_people, + route: RouteConstants.mintNftPath, + ), ]; } \ No newline at end of file diff --git a/lib/utils/constants/route_constants.dart b/lib/utils/constants/route_constants.dart index ef4a87f..161dcab 100644 --- a/lib/utils/constants/route_constants.dart +++ b/lib/utils/constants/route_constants.dart @@ -1,7 +1,12 @@ class RouteConstants { static const String home = '/'; static const String allTrees = '/trees'; + static const String mintNft = '/mint-nft'; + static const String mintNftDetails = '/mint-nft/details'; static const String homePath = '/'; static const String allTreesPath = '/trees'; + static const String mintNftPath = '/mint-nft'; + static const String mintNftDetailsPath = '/mint-nft/details'; + } \ No newline at end of file From ba804f1d530eb654fdc579d3b877e44e82c3d6d3 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sun, 20 Jul 2025 19:48:34 +0530 Subject: [PATCH 03/18] add: add mint_nft_details page --- lib/main.dart | 3 +- lib/pages/mint_nft/mint_nft_details.dart | 36 ++++++++++++------------ lib/providers/mint_nft_provider.dart | 6 ++++ lib/providers/wallet_provider.dart | 4 --- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 95bf1db..fcafa39 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/pages/home_page.dart'; +import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; @@ -51,7 +52,7 @@ class MyApp extends StatelessWidget { path: 'details', // This will be /trees/details name: '${RouteConstants.mintNft}_details', builder: (BuildContext context, GoRouterState state) { - return const MintNftCoordinatesPage(); + return const MintNftDetailsPage(); }, ), ] diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 29fa7e0..33bf185 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -13,14 +13,14 @@ class MintNftDetailsPage extends StatefulWidget { } class _MintNftCoordinatesPageState extends State { - final latitudeController = TextEditingController(); - final longitudeController = TextEditingController(); + final descriptionController = TextEditingController(); + final speciesController = TextEditingController(); - void submitCoordinates() { - final latitude = latitudeController.text; - final longitude = longitudeController.text; + void submitDetails() { + final description = descriptionController.text; + final species = speciesController.text; - if (latitude.isEmpty || longitude.isEmpty) { + if (description.isEmpty || species.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Please enter both latitude and longitude.")), @@ -28,14 +28,14 @@ class _MintNftCoordinatesPageState extends State { return; } Provider.of(context, listen: false) - .setLatitude(double.parse(latitude)); + .setDescription(description); Provider.of(context, listen: false) - .setLongitude(double.parse(longitude)); - latitudeController.clear(); - longitudeController.clear(); + .setSpecies(species); + speciesController.clear(); + descriptionController.clear(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Coordinates submitted successfully.")), + const SnackBar(content: Text("Details submitted successfully.")), ); context.push(RouteConstants.mintNftDetailsPath); } @@ -54,25 +54,25 @@ class _MintNftCoordinatesPageState extends State { ), const SizedBox(height: 20), TextField( - controller: latitudeController, + controller: descriptionController, decoration: const InputDecoration( - labelText: "Latitude", + labelText: "Description", border: OutlineInputBorder(), constraints: BoxConstraints(maxWidth: 300), ), ), const SizedBox(height: 10), TextField( - controller: longitudeController, + controller: speciesController, decoration: const InputDecoration( - labelText: "Longitude", + labelText: "Species", border: OutlineInputBorder(), constraints: BoxConstraints(maxWidth: 300), ), ), const SizedBox(height: 20), ElevatedButton( - onPressed: submitCoordinates, + onPressed: submitDetails, child: const Text( "->", style: TextStyle(fontSize: 20, color: Colors.white), @@ -86,8 +86,8 @@ class _MintNftCoordinatesPageState extends State { @override void dispose() { - latitudeController.dispose(); - longitudeController.dispose(); + descriptionController.dispose(); + speciesController.dispose(); super.dispose(); } } diff --git a/lib/providers/mint_nft_provider.dart b/lib/providers/mint_nft_provider.dart index 30839c7..b6c5968 100644 --- a/lib/providers/mint_nft_provider.dart +++ b/lib/providers/mint_nft_provider.dart @@ -4,6 +4,7 @@ class MintNftProvider extends ChangeNotifier { double _latitude = 0; double _longitude = 0; String _species = ""; + String _description = ""; String _imageUri = ""; String _qrIpfsHash = ""; String _geoHash = ""; @@ -31,6 +32,11 @@ class MintNftProvider extends ChangeNotifier { notifyListeners(); } + void setDescription(String description) { + _description = description; + notifyListeners(); + } + void setImageUri(String imageUri) { _imageUri = imageUri; notifyListeners(); diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 648fdf1..283a719 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -5,9 +5,6 @@ import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:tree_planting_protocol/models/wallet_option.dart'; -import 'package:http/http.dart' as http; -import 'package:tree_planting_protocol/utils/services/wallet_provider_utils.dart'; -import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; enum InitializationState { @@ -25,7 +22,6 @@ class WalletProvider extends ChangeNotifier { InitializationState _initializationState = InitializationState.notStarted; String _statusMessage = 'Initializing...'; String? _currentChainId; - final Map _rpcUrls = rpcUrls; static const String _defaultChainId = '11155111'; From b9e65ce6cc691dd479e6be3a0b6ac3850c985a48 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sun, 20 Jul 2025 23:15:43 +0530 Subject: [PATCH 04/18] add: add ipfs image upload page --- lib/main.dart | 8 + lib/pages/mint_nft/mint_nft_details.dart | 2 +- lib/pages/mint_nft/mint_nft_images.dart | 365 +++++++++++++++++++++++ lib/utils/constants/route_constants.dart | 2 + lib/utils/services/ipfs_services.dart | 38 +++ pubspec.lock | 120 ++++++++ pubspec.yaml | 1 + 7 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 lib/pages/mint_nft/mint_nft_images.dart create mode 100644 lib/utils/services/ipfs_services.dart diff --git a/lib/main.dart b/lib/main.dart index fcafa39..b2faf85 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; +import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_images.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; @@ -55,6 +56,13 @@ class MyApp extends StatelessWidget { return const MintNftDetailsPage(); }, ), + GoRoute( + path: 'images', // This will be /trees/details + name: '${RouteConstants.mintNft}_images', + builder: (BuildContext context, GoRouterState state) { + return const MultipleImageUploadPage(); + }, + ), ] ), diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 33bf185..6fd5e97 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -37,7 +37,7 @@ class _MintNftCoordinatesPageState extends State { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Details submitted successfully.")), ); - context.push(RouteConstants.mintNftDetailsPath); + context.push(RouteConstants.mintNftImagesPath); } @override diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart new file mode 100644 index 0000000..5681ad3 --- /dev/null +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -0,0 +1,365 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; + +class MultipleImageUploadPage extends StatefulWidget { + const MultipleImageUploadPage({Key? key}) : super(key: key); + + @override + _MultipleImageUploadPageState createState() => _MultipleImageUploadPageState(); +} + +class _MultipleImageUploadPageState extends State { + final ImagePicker _picker = ImagePicker(); + List _selectedImages = []; + bool _isUploading = false; + int _uploadingIndex = -1; + List _uploadedHashes = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = Provider.of(context, listen: false); + setState(() { + _uploadedHashes = List.from(provider.getInitialPhotos()); + }); + }); + } + + Future _pickImages() async { + try { + final List images = await _picker.pickMultiImage(); + if (images.isNotEmpty) { + setState(() { + _selectedImages = images.map((image) => File(image.path)).toList(); + }); + } + } catch (e) { + _showSnackBar('Error selecting images: $e'); + } + } + + Future _uploadAllImages() async { + if (_selectedImages.isEmpty) { + _showSnackBar('Please select images first'); + return; + } + + final provider = Provider.of(context, listen: false); + List newHashes = []; + + for (int i = 0; i < _selectedImages.length; i++) { + setState(() { + _uploadingIndex = i; + }); + + try { + String? hash = await uploadToIPFS(_selectedImages[i], (isUploading) { + setState(() { + _isUploading = isUploading; + }); + }); + + if (hash != null) { + newHashes.add(hash); + setState(() { + _uploadedHashes.add(hash); + }); + } else { + _showSnackBar('Failed to upload image ${i + 1}'); + } + } catch (e) { + _showSnackBar('Error uploading image ${i + 1}: $e'); + } + } + + setState(() { + _uploadingIndex = -1; + }); + provider.setInitialPhotos(_uploadedHashes); + + if (newHashes.isNotEmpty) { + _showSnackBar('Successfully uploaded ${newHashes.length} images'); + } + } + + Future _uploadSingleImage(int index) async { + if (index >= _selectedImages.length) return; + + setState(() { + _uploadingIndex = index; + }); + + try { + String? hash = await uploadToIPFS(_selectedImages[index], (isUploading) { + setState(() { + _isUploading = isUploading; + }); + }); + + if (hash != null) { + setState(() { + _uploadedHashes.add(hash); + _uploadingIndex = -1; + }); + + final provider = Provider.of(context, listen: false); + provider.setInitialPhotos(_uploadedHashes); + + _showSnackBar('Image ${index + 1} uploaded successfully'); + } else { + _showSnackBar('Failed to upload image ${index + 1}'); + setState(() { + _uploadingIndex = -1; + }); + } + } catch (e) { + _showSnackBar('Error uploading image ${index + 1}: $e'); + setState(() { + _uploadingIndex = -1; + }); + } + } + + void _removeImage(int index) { + setState(() { + _selectedImages.removeAt(index); + }); + } + + void _removeUploadedHash(int index) { + setState(() { + _uploadedHashes.removeAt(index); + }); + final provider = Provider.of(context, listen: false); + provider.setInitialPhotos(_uploadedHashes); + } + + void _clearAll() { + setState(() { + _selectedImages.clear(); + _uploadedHashes.clear(); + }); + final provider = Provider.of(context, listen: false); + provider.setInitialPhotos([]); + } + + void _showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + @override + Widget build(BuildContext context) { + return BaseScaffold( + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isUploading ? null : _pickImages, + icon: const Icon(Icons.photo_library), + label: const Text('Select Images'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton.icon( + onPressed: (_selectedImages.isEmpty || _isUploading) + ? null + : _uploadAllImages, + icon: const Icon(Icons.cloud_upload), + label: const Text('Upload All'), + ), + ), + ], + ), + + const SizedBox(height: 16), + if (_isUploading) + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 8), + Text( + _uploadingIndex >= 0 + ? 'Uploading image ${_uploadingIndex + 1} of ${_selectedImages.length}...' + : 'Uploading...', + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + if (_selectedImages.isNotEmpty) ...[ + Text( + 'Selected Images (${_selectedImages.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _selectedImages.length, + itemBuilder: (context, index) { + return Container( + width: 120, + margin: const EdgeInsets.only(right: 8), + child: Card( + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + _selectedImages[index], + width: 120, + height: 80, + fit: BoxFit.cover, + ), + ), + + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => _removeImage(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), + ), + Positioned( + bottom: 4, + left: 4, + right: 4, + child: SizedBox( + height: 28, + child: ElevatedButton( + onPressed: (_isUploading && _uploadingIndex == index) + ? null + : () => _uploadSingleImage(index), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + ), + child: (_isUploading && _uploadingIndex == index) + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.upload, size: 16), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: 16), + ], + if (_uploadedHashes.isNotEmpty) ...[ + Text( + 'Uploaded Images (${_uploadedHashes.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + ], + Expanded( + child: _uploadedHashes.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_off, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No images uploaded yet', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: _uploadedHashes.length, + itemBuilder: (context, index) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + child: Text('${index + 1}'), + ), + title: Text( + 'Image ${index + 1}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + _uploadedHashes[index], + style: const TextStyle(fontSize: 12), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.open_in_new), + onPressed: () { + // You can implement opening the IPFS link here + _showSnackBar('IPFS Hash: ${_uploadedHashes[index]}'); + }, + tooltip: 'View on IPFS', + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _removeUploadedHash(index), + tooltip: 'Remove', + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/utils/constants/route_constants.dart b/lib/utils/constants/route_constants.dart index 161dcab..d315cf7 100644 --- a/lib/utils/constants/route_constants.dart +++ b/lib/utils/constants/route_constants.dart @@ -3,10 +3,12 @@ class RouteConstants { static const String allTrees = '/trees'; static const String mintNft = '/mint-nft'; static const String mintNftDetails = '/mint-nft/details'; + static const String mintNftImages = '/mint-nft/images'; static const String homePath = '/'; static const String allTreesPath = '/trees'; static const String mintNftPath = '/mint-nft'; static const String mintNftDetailsPath = '/mint-nft/details'; + static const String mintNftImagesPath = '/mint-nft/images'; } \ No newline at end of file diff --git a/lib/utils/services/ipfs_services.dart b/lib/utils/services/ipfs_services.dart new file mode 100644 index 0000000..975d3e1 --- /dev/null +++ b/lib/utils/services/ipfs_services.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +String API_KEY = dotenv.get('API_KEY', fallback: ""); +String API_SECRET = dotenv.get('API_SECRET', fallback: ""); + +Future uploadToIPFS(File imageFile, Function(bool) setUploadingState) async { + setUploadingState(true); + + var url = Uri.parse("https://api.pinata.cloud/pinning/pinFileToIPFS"); + var request = http.MultipartRequest("POST", url); + request.headers.addAll({ + "pinata_api_key": API_KEY, + "pinata_secret_api_key": API_SECRET, + }); + + request.files.add(await http.MultipartFile.fromPath("file", imageFile.path)); + var response = await request.send(); + + setUploadingState(false); + + if (response.statusCode == 200) { + var jsonResponse = json.decode(await response.stream.bytesToString()); + return "https://gateway.pinata.cloud/ipfs/${jsonResponse['IpfsHash']}"; + } else { + return null; + } +} + +// Usage in your main file: +// String? result = await uploadToIPFS(imageFile, (isUploading) { +// setState(() { +// _isUploading = isUploading; +// }); +// }); \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index b3fc06f..3bb8eb1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: "direct main" description: @@ -233,6 +241,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + url: "https://pub.dev" + source: hosted + version: "0.9.4+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" fixnum: dependency: transitive description: @@ -270,6 +310,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" flutter_svg: dependency: transitive description: @@ -320,6 +368,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" + url: "https://pub.dev" + source: hosted + version: "0.8.12+24" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" js: dependency: transitive description: @@ -416,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0f46e13..8cb2d1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: go_router: ^15.2.4 cupertino_icons: ^1.0.8 flutter_dotenv: ^5.1.0 + image_picker: ^1.0.4 dev_dependencies: flutter_test: From d48c940cd5367f1a1a5c3ca51ac5b337b714cb78 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sun, 20 Jul 2025 23:51:18 +0530 Subject: [PATCH 05/18] modify navbar design --- assets/tree-navbar-images/logo.png | Bin 0 -> 153025 bytes assets/tree-navbar-images/tree-1.png | Bin 0 -> 2186 bytes assets/tree-navbar-images/tree-10.png | Bin 0 -> 2250 bytes assets/tree-navbar-images/tree-11.png | Bin 0 -> 1666 bytes assets/tree-navbar-images/tree-12.png | Bin 0 -> 2134 bytes assets/tree-navbar-images/tree-13.png | Bin 0 -> 3486 bytes assets/tree-navbar-images/tree-2.png | Bin 0 -> 2098 bytes assets/tree-navbar-images/tree-3.png | Bin 0 -> 2319 bytes assets/tree-navbar-images/tree-4.png | Bin 0 -> 2015 bytes assets/tree-navbar-images/tree-5.png | Bin 0 -> 2735 bytes assets/tree-navbar-images/tree-6.png | Bin 0 -> 2750 bytes assets/tree-navbar-images/tree-7.png | Bin 0 -> 1826 bytes assets/tree-navbar-images/tree-8.png | Bin 0 -> 2779 bytes assets/tree-navbar-images/tree-9.png | Bin 0 -> 2330 bytes lib/components/universal_navbar.dart | 674 ++++++++++--------- lib/pages/mint_nft/mint_nft_coordinates.dart | 5 +- lib/pages/mint_nft/mint_nft_details.dart | 11 +- pubspec.yaml | 2 + 18 files changed, 375 insertions(+), 317 deletions(-) create mode 100644 assets/tree-navbar-images/logo.png create mode 100644 assets/tree-navbar-images/tree-1.png create mode 100644 assets/tree-navbar-images/tree-10.png create mode 100644 assets/tree-navbar-images/tree-11.png create mode 100644 assets/tree-navbar-images/tree-12.png create mode 100644 assets/tree-navbar-images/tree-13.png create mode 100644 assets/tree-navbar-images/tree-2.png create mode 100644 assets/tree-navbar-images/tree-3.png create mode 100644 assets/tree-navbar-images/tree-4.png create mode 100644 assets/tree-navbar-images/tree-5.png create mode 100644 assets/tree-navbar-images/tree-6.png create mode 100644 assets/tree-navbar-images/tree-7.png create mode 100644 assets/tree-navbar-images/tree-8.png create mode 100644 assets/tree-navbar-images/tree-9.png diff --git a/assets/tree-navbar-images/logo.png b/assets/tree-navbar-images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb199c28ea6d3e90e1093f7cc398cf27b31719d GIT binary patch literal 153025 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%0AEghR< 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 literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-1.png b/assets/tree-navbar-images/tree-1.png new file mode 100644 index 0000000000000000000000000000000000000000..66d3c1496b08b9cba175bcdd70efd75daa94844f GIT binary patch literal 2186 zcmV;52zB>~P)sAK~#7Fomg9J z99J3s&)oKY@nzR`;s)C_aSFz2Wrfm~loXW;DyR?-=>wvGAVNY=@eV>rJo5lAR3Jgb z142})DphKw1_-EB4UmvDPFkn28)swht}knQz4qSba{B*gc6MjiGrQ{^X}z=O{O9|> z^Iy*xz&%*om*-$zBv{8h^Lk+JZ!c;4TXqXDPi6fK7v26g_(1fvne|v@N09cHY95Il zVN;hC);7%?WVa;i!Ikw?(0bzH5A;Bzk9GQM6JI!SX5?q-(a579OA(HnnZ-+6>-$&o z|1JFdt-mh6je;_wS#HplhHkoC{-E=5-gSe5ALHp4XU{$K+N;jqmX3Cojz+g;*2f8mzC8Z9arL<$%`8<8j4{28gcy^+_q;@a zTUuC^)S#TX^6N{}SFZ|62Ka75H7LTHD!UKiy2sCddhWB&jbGbX+BYN#6vi7~?_*dN zm?P6M`JLD9&0qfA;=8!h>=`i7Gntrl?w9jlOw9~j+|8EJ6p~ zyS4kJSAKEj5>A;t^kN zO~${i1(jDaHwp!?lfrjMowNiNl9->LXY*g5{{K$4LUAE`=yMyzFFM^T@=o@^IQ@;O z>l0@u6lNzxzr^gc#27Rl{mJ0%je8{%B@}4G2T(N?WJM9*ojg=k1*L7xRgom7Y^>}V z&z}F}9d0LrZ(PV;bI<&A?yHfo_CwROq&BS};suuPEJFdkv9rAc1A_xC5j6GE2TQoN z1>1#fNG6hEu8q9JB*cg%PeyO9T;5y+A7e`%l+9C*jJ~YbEQ?|4S(sL;qEhBoYy_&QMhiEYlJ)X3gTCdHndFkdozRvkr?fcJ_s-&&S5% z=Qs0PEOS2t#oZ#Tudl<{*q9(mBc6!E;NYMj=*$UgBKcS>Cf1_ysJ+H**S~l378Hv` z*xcMC!YmL7sahERwR&?m<7KrrVnqIl@0|LJS+~Z+kuZ}a8G?a;AmzRiS=6t8*%jIzI8r19cCSFs5Nr9P}p%{3KRxLsK7*_{P zqt&f=K3PRzKy@qMEy|oQ{dwT+{hWj6!oC4j{ zn>Mm^3zVovXU8zCB4-(<*TcIbYGn~lb$prgZ?rU|L^ zFpQ3k2uvblnym|jX|ejP%L4^6!^!&n&!gKSzc<{3_G=bl9c}oCWy)x1PEh zjA(Pa8wXrw_GQGWV=@Nx1~zz7PA*PjKTM_5So0d|*lQcT(n_R5QnjitUc7Mkk(Gb0 zP$n5&bc5%A5ZcHgz}!>EW0b^JY*f${~)&F>qt-rOEDPCH;}9{%TUdU0G#^#vS<~W& zY7ovrOp#~%BEon#VytZwhdwd_i&?SCUrK6DV{j)_8H$GSdv6%`B>+192h={i z3M_!Xd~@8b)Uu-i^?E9#JQI?oPyk~~m8HGqQvI8+ukZa0x;U`6O6ECx6bz~3H#Fvs zh;(Ek>?p!VkYruu*rA7yF__m3W4(kv4zQmD>tBJZ)-a!Cze#89BXKL0p|Pj?ApigX M07*qoM6N<$g6i5VEC2ui literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-10.png b/assets/tree-navbar-images/tree-10.png new file mode 100644 index 0000000000000000000000000000000000000000..3c6cea7673ddbd0cbe782104849f1a4d08a7744b GIT binary patch literal 2250 zcmV;*2sQVKP)vOE^#sTK%oa5V}Ts18;pF;z1q#?)FIqoq!wnbu6IQxj_*O%ttcTE|Ih zCt~8@qiLDV3hb-*gdr!~Z2YdH%cNfjiu=k#G{{KJU z|NrOw=RX%VT$5ea&thS!x$?$c(+?KKUM|E)M~E2^d$us5|N1~20z##xQQOz zBK_~H{NKfE+4X4HN@y=e3IfM_RM2JtK3uPE`&D*vCe^A4RiBHn9CmL3|I5bC%@pl= zBqJ_4>=ou7CxSx+6Sz94fbt>4r=&(&UtN4Gm3E8KvN?3gY&t}I=9Aep;DOY$>9GRE zxd<72mgAeugu#dv=)heL!S;3)mt%-#UospNSw(Oh1SRAEDVZDlF!&Mj^eRzGm{IXt zk3K1wMi~(tPZ=>oU`K~W$eI<-mcDx}jdC%efwSxOSzXvK6yhZ$WdX3hcWcYQ#{J87 z(RU#)JL)AgXpbB)N&{y@pw6bg7r!`EWu)S~L^d$_i7S?cB16yY%w65zJ`#yB zN*3F!5=Yv%F#V|;VxfH$1hs~}xxe7_0ou(Uej{V$sXyjycB*FrB6Dxvi9ReDcJs>H zO4653ds`c>hKt4j-65{P-k$MV(M4it-!Ul#gF%nE3&-Y*%%fcejfEQ@DqEGGRAj8o z|46h$)Sg#phr6-%G>halk`1_|Dvj0#WlTrEC70zimN%w9SJD0N)t; zq#1v#E|2G|n>){Pj|bz|-TwgRc7FX(ee3Wb-&{G2xUg7tU-1hUio5Dol_+{GGqCe$ z6LZgSi&=QNywJ?BxPY)|sPahN8VI4D!2yP`hanW@v^4#bgcqQ_q4#5u_<3iu;;vzu zmaTZdcKO9axyuqGR8M10aDpSyNz=~eHZ4S}Ai`=P2YzYzAmZPp%_b*y2;`)RqfV?B zh}&(f!H|xZY)>tteZxi1on0+geTT!ALobw7Y^2}J^Wh6GGej-!vG^t zi(4!V?72h1&DSf=?X5^CJ94wv=^DY2)Ew|gfOFq>7gJiae2!ihtzmm_4{qqF>3x2n zdn9uG-esEzLdww*>phq3?Afz*;fZ0{2&1m@kLv1j7DoLP2Cep$e}{(k?M|gF=MlZ) z%?p9%K65jD=`hob)vmj-IB8Y{Pv{@RB704@;EB|kw5>I@_I~b?syNfO z?*xf~Semps8wk;fy7ah*qGqTyisygvx2KB0BUql*h}2oGz|5l z=m}W-Ov=w~V?5M-*8Vy;?J=&(_*wPsQ9d#;3=~uG^>cszPvVY@4^d>CZagV{Y9UBV zatP&c4v+`Eg!#{ks_QgiO3|VAqBCwz(%P^{HJmG|zAEZj^rQI+>6!6=h>q7XUmUGT z+Oy>Ek5>Gp(IhJBzcW}P@Sx1fO4Lf-HG!7M%xcd^GRXWyo=~EtJkf|1^OQ(sLVw># zi<6aK^G0?9XXD`N(dK#oI#6wL%7~>uaAU+AGhIiUcWi;K68^?mDKdLTBHmknqD?rS za8YqO?cB#_BwI$tp}nf7R2+EmNdE5Lj^S{IA3ss@r^D zG8xBx4W!$iIG^5DxPc|y>4cbFClNJ%$Koaq~L-P3R8mN5zj&g{Ol0y(@#{C{!? zC80P|cc42-U?MQ^&CoSk;@cIe)Ht0*c;P_Tz{RTb?cKyxeEN+;@SywULcsufS>E40 z^0h+4+9hc$*(jQjKRsN%j%sEmH*+v@=eocEy>@h0kEK~XMKvKjGGJ+_Khq}WJU+-x zdy?c;Zh4a2mo5aiF5DW7jGr>DPt-xeEIk-&Odo*v*Afyc#s{(lNq#-Ze#|B3C9AVC z#be7(A+KX}pJNgx^Z?Q_qE7DznfP0v`%p3&5t(bQFH7j(C*-BR6^Bd)#DlAvtl4h* YKO;*$miD*0I{*Lx07*qoM6N<$f`em2asU7T literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-11.png b/assets/tree-navbar-images/tree-11.png new file mode 100644 index 0000000000000000000000000000000000000000..276d00b6f7f53d93017f488aae35b02da1cdce52 GIT binary patch literal 1666 zcmV-|27UR7P)F`hui3 zH4rK^M5s*(B^N6ZKXOG^NnO`=dF$%-)_wavX71_E%=@@=XXed3TRJdr?z{JV{O-Bu zoO=dfB8fN+3H%R4xL@bBiLp>6194d-+sbAXHm#zYt^aWPZh4@vN8NsTBON$5tbs%nCqqh5#&B)$ z%$DW5TfmH8kK=}_QA=fJ;Nj|ql*2nuElf;{DJ4z3i*%-(Mn;qA`8#@F)}DA}@yx=; zz+e9RbE)h!Sx`@VyKf{HKiOQzj&hlnr`%GRkw`=bsV-VG=M8B%yZzFiNqut_W3p1= zaVje|;$n_`u=d+cszg7Y+)ziDS*kBm6=`g84TK`WJ{F6r9s>R6Z>lTX+xkHFJ`p!U zEGdbCEf?EXz!f4w&t$sdwfPS&dA#|XL__WUnSXLaN543|^vHKTgYTbsaq!lS)MVyK zB^VCS%O?gm?%n*$S5Wezq1i~hIjiS{>LqgDq}j1qR8qp3o9U5UK65>LIo?;Ot))6_ z1L#Or)Rrp7NY*P^OG^uRu5(dt@akv#5azTUOX zlp)9+pAW*)4?j@^ix+;l+ELdSG5)27(oo;ru(q&9U-M}|-$x#aeoz`40 zz4PdvrC^pQDn?z^0I~GOk|`a@cq*GR;UYxushOt_-s*bh%Sb%1nlU)$U$8bndX8P& z0A{F}MV?q$6}wnDGYlV~Kfqd68)2`6`3G}betLGlrfF-&vsA1cML{((C3JD;${%-H z*3kk%2PhVp@))bQT!~CF#c?niSjZZU1yn=bcUmYGkh6D;N^iti^qvqQveeT)zjFcm zA7D@33cjpgvfW=!WXTC}qQJt1Hq}@~rv^B+epa1)y6f61Ce|HqjAkN<(BG%J{(5T5 zL%VfwiVd8w`BD==n7fec&s+;PI;k;c6w4MF#z`|`%DF>*%MWbp{`~;JA#PPNvJ8z| zM^J>8DXC}_`8QO)$_)i#TaHxWY(PysB=z+q*YA4s&~Y}S2`XqQ%Y+CubD!VIU68pM zYg`M~H5R}}K3#us4+vM6VCI>6%y!U)tZRtt<%zikqQ6>B94DtK5@8))+sTGn6<9ii(UZ5;wuIb3J}q@Zqr1H zRe;#fS-3&mW3ZTJZpEnL3{T}0wPUuT63wAYh?VYQf?kGg#mGW(P*%)1CMXA$FjOe+ zLZxwFB?3V3nE=GjU4(w+?9eCCL}<7+9vZoFCb`3=u@I*O?15ru^-;lX0Rk1vj0E=2 zK8g5H?PJ=F%_qBWBKEg`6NF0?n0Rbez~F~fUkue6ahJAwf>coUFCz$FeE{=J6#xJL M07*qoM6N<$f+!;g5dZ)H literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-12.png b/assets/tree-navbar-images/tree-12.png new file mode 100644 index 0000000000000000000000000000000000000000..8a0ab76d8e2845c4bf6afe1e759a5b9c089ba57f GIT binary patch literal 2134 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 M07*qoM6N<$f(NOc(EtDd literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-2.png b/assets/tree-navbar-images/tree-2.png new file mode 100644 index 0000000000000000000000000000000000000000..0b50ee355cccb366ff59ef0bb7a08da343e12abf GIT binary patch literal 2098 zcmV-22+jA2P)w&U2ble$Ucd^Bm+uFKl4l+p=l9k8}&6_bVpqLY}!Kuk#N0a18Fs>A~? z&=>lG_!wvs2!zBCjFpU`_5m1(L@YqlHA$ClwwESx?AX57xxV+FvvZCef8Jc%?H=~G zu6^(M`2TQZ_f6fQ zibAlV7h&PX0^D4<3A$kb2aGMlp(95JE>BO#g5}Mq#-lYC)g8T>8SXvO((4TU`;A%H z&QdHi)*I@DLk}L3bBT8o{CbzmLD-+b@SeGj8;`eh%iNi>vPTLfRRN;im)EGah-7YO zwjS93$qln;?I8-0%xv41lcVFKfO)hyjssdiO)k7+T$kRKHU1d!v*!{yndgTi%m8W5uJ3zKZpVBdP>e7*%-2hoCB9~rdKpmyQ}u(4;3 zluCIAOw)v-QLIHMcW}zpCVm4w0SVq0Ch*QLHEnKo7IChnZcI!X;qAn(=jKxQ)A5zN zcQzcXU`2r036kOXuw0Nzrl3$LfT0^@@S9+V&Y@*C3uDlpzZ<~vi8|OuCeKU;4<4Er zy_?v;VuR1`gFqm_U6092(D2Bx#IRl52D4;}donmWuy5sQHL~*ZN&$B9LpQy4nADMU z{;9`?Vo!bh_}bd?mW35X1z|8+<|2G9nMz6tLX#q*cT6paI?UR8Af&!>c|LaQS}F}4 zsRuF>8F_s4Son{kyoqN4N~{y+^!xp=e_}uM5A;h- zZkyYns;aEbg1CukbUg4hQmpHnx2-$SAt~)pRfa_{(R0r~@=_shj8liog(8LY_4PqK z9*0OIA~h%?IvR~iprV=x4U0=~!AyaJM<=HKJCpbslmnE42cZrdR@sn#?Ke-%8EfWI zj1*<3(-BsZptz5U5!uOzamwVJFt;?nn8oFB%BT|_(B8cA%E#@?F%UX`04Z4Pq&lZ+ReBm zqY~JKzE}?_qE}MgNIxV#*OfD#uN&#NQ(#+v-SVpnn$+ahf#|rK7 zsar*(0A9i?x5yJ&?GW~Zr=v#>oQcg_+vTpMR4PgH3KNQ`m34@gIiia5R2o;{6-R;> zK-B<(xBr%#zHxQ+246y0o8|$vo8(@hjC>+AaqN6-G5?-vbLp*S=Bvz-1+iF6KCiE@ zOF*raT~r&e!*L(E_3FlfrPp)suxbm|EVp*=1&>UBe%e3ru~I&lDptVNUT=h1g*JVf zPnI{4R>ZV0eUjJX5^p4g)==~7EZ^3*FF@}6#os$WGtX;bMT;&*BsGl9=4IsAXATZN z@ujiE%A0F8Wd@;6?F~|BC>RVvv_C3=S(YVDF4&0lGC4UR-9S_sQC-A5&~kR>`ID%2 zuYKklLj_RMqw}vOS0za1uBP3zhc5o%NxHhYWg`U>QPwCc0~8t&ABUDJN%e&v)g4ym z8zzHEksUX!q#v#o?zaLkqma{mcs4BASTANuX_PcZk?tT=Dj$IPc>eDpjE?jq{C(9;8aurAaNo122>r;1*0KrXt-?CJnFaIqP-ZAMJ2z`R zs;VD(v_CjoL}d+1f&64W{P*u^{iFZ1tXckwfM1#!+!GFJV_M92I6BgEh#D*gLD)$QW}#KloX+kIee;2k?*}$*v=8B_XEASHd~Gd%*_)Yn&w$(Nv3Z(7 z1J7PndDx+>*~qwT!1BJDf;{*kuYoSYq}zx>9^=SsL-(QU!lbJusM`(qwx)AJ_a?LX cZv_7QJ}CG=VL0to_W%F@07*qoM6N<$f(M2Sn*aa+ literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-3.png b/assets/tree-navbar-images/tree-3.png new file mode 100644 index 0000000000000000000000000000000000000000..986bafdc21209919af060fabcd760588722d4e83 GIT binary patch literal 2319 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 Eg4F3K5C8xG literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-7.png b/assets/tree-navbar-images/tree-7.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbbea0adc9842f3f11cc511d2e5d3449335943c GIT binary patch literal 1826 zcmV+-2i^FIP)K~#7FtypVp z6jc>~#Gt<<20}t=BK|NY8Z`$0 z_@D47QhxX#*pToDwIGHTLZg*#Q9>vd83P&@l=-#N}i(2M{)n5|pptSqJhK=6Zee z(JRW!-c3HvF!h-OSf`SR7_8d^qVj4__H&M+6$QqQ8O|{?&epB}ynx`1VKEMpZ?5aa z#Nb6+u*tTAqoFncIU}Qk5~T;~pBlYK(U6QV;2O6NnCoYl6jsASZf=sU(%S&(q0Bon zwUf}}HLLgiwtVCFn-|x(RC1>luQ@QxN8|)yKzJlRgHI@9D)!}h^Sr;*Z}|3yMXTOM zHstKR0b)(wb??a3PRgTrbo{My|AF6&=N@=iP}5n#dACo?LV7+X%g{Qf^6S2a%`es! zm+p=TG1L{D`We{>STTm=2QhHQh7VI)lP3iCahOH@{0wrj+)jlYG1>w$C5xM^;3v$U>$~v>v7=3<{L=4Bi`akyQEt=p2Dw5hv!9f3Jcao*Eg#22wbC zHl}b?4mvxzy$Hplk`&J9>1~+3gcNKSLZTL(a^56JZHlRe^Ose+t(H?@05jT-V z$vV^@i*%!k4Azh!zGiMfT;x{y!7%~us@Lr)q|Bw(4-W9a(R0EMz5<lx zj=BS;EI>lCFrV{sPX%5jMs(zX!9=mGx-nAtR4(B)LLix>vo2nRphKi+>+UA)d*$|6uU z6;N>9uA$LLWIn-}JatwjsTF6_Q$r^sxyc%o`v2s|2E))>w_H@?t@xgtyg(W0$5DlJ843Qzzvgq9m9NBWR$9d@{-K{rj{f5 zJ%!A}$aKf4+g)$8C%r=I>D*G&PSh=yJ}z70KT^B&5+h5Crvy4zh~}^yIy*hO=e>tF zI#6OaKt!_$L1}VXgPHig!H6WDe1b6@ewz+5St z_pEV8NUaOy-RFIJdu?@f?vil0Y86wuqTXB7y0*0XwS$2Nt4@yfhSC2dj(#P0*0?SF zY|VDcDZS_EK9lZdN#NNXC!d;SdlPhn-U8dcl&)MqbC60;a;{oS5@4yGnRbf^eS!VN&YBr-)uX#fBK literal 0 HcmV?d00001 diff --git a/assets/tree-navbar-images/tree-8.png b/assets/tree-navbar-images/tree-8.png new file mode 100644 index 0000000000000000000000000000000000000000..13d0a7650558ce78d50351e4d1e34937d6461237 GIT binary patch literal 2779 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 Ar2qf` literal 0 HcmV?d00001 diff --git a/lib/components/universal_navbar.dart b/lib/components/universal_navbar.dart index 08afb87..782c94f 100644 --- a/lib/components/universal_navbar.dart +++ b/lib/components/universal_navbar.dart @@ -13,352 +13,406 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { const UniversalNavbar({super.key, this.title, this.actions}); @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); + Size get preferredSize => const Size.fromHeight(120.0); + + // Updated to use PNG file names instead of icons + final List treeImages = const [ + 'tree-1.png', + 'tree-2.png', + 'tree-3.png', + 'tree-4.png', + 'tree-5.png', + 'tree-6.png', + 'tree-7.png', + 'tree-8.png', + 'tree-9.png', + 'tree-10.png', + 'tree-11.png', + 'tree-12.png', + 'tree-13.png', + ]; @override Widget build(BuildContext context) { final walletProvider = Provider.of(context); final themeProvider = Provider.of(context); - - return AppBar( - title: Text(title ?? ''), - actions: [ - // Theme toggle button - IconButton( - icon: Icon( - themeProvider.isDarkMode - ? Icons.light_mode - : Icons.dark_mode, + return Container( + height: 140, + decoration: BoxDecoration( + color: themeProvider.isDarkMode ? const Color.fromARGB(255, 1, 135, 12) : const Color.fromARGB(255, 28, 211, 129), + ), + child: Stack( + children: [ + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 40, + child: _buildPlantIllustrations(), + ), ), - onPressed: () { - themeProvider.toggleTheme(); - }, - tooltip: themeProvider.isDarkMode - ? 'Switch to Light Mode' - : 'Switch to Dark Mode', - ), - - // Chain selector (only show when connected) - if (walletProvider.isConnected) - _buildChainSelector(context, walletProvider), - - ...?actions, - - // Wallet connection/menu - if (walletProvider.isConnected && walletProvider.currentAddress != null) - _buildWalletMenu(context, walletProvider) - else - _buildConnectButton(context, walletProvider), - ], + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Image.asset( + 'assets/tree-navbar-images/logo.png', // Fixed path to match your folder structure + width: 28, + height: 28, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.eco, + color: Colors.green[600], + size: 28, + ); + }, + ), + ), + const SizedBox(width: 8), + if (title != null) + Flexible( + child: Text( + title!, + style: const TextStyle( + color: Color.fromARGB(251, 179, 249, 2), + fontSize: 30, + fontFamily: 'Poppins', + fontWeight: FontWeight.bold, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 2, + color: Colors.black26, + ), + ], + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + Expanded( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon( + themeProvider.isDarkMode + ? Icons.light_mode + : Icons.dark_mode, + color: Colors.white, + size: 18, + ), + onPressed: () { + themeProvider.toggleTheme(); + }, + tooltip: themeProvider.isDarkMode + ? 'Switch to Light Mode' + : 'Switch to Dark Mode', + ), + ), + + const SizedBox(width: 6), + if (actions != null) ...actions!, + + if (walletProvider.isConnected && walletProvider.currentAddress != null) + _buildWalletMenu(context, walletProvider) + else + _buildConnectButton(context, walletProvider), + ], + ), + ), + ], + ), + ), + ), + ], + ), ); } - Widget _buildChainSelector(BuildContext context, WalletProvider walletProvider) { - return PopupMenuButton( - icon: Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.blue[800] - : Colors.blue[50], - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.blue[600]! - : Colors.blue[200]!, - ), + Widget _buildPlantIllustrations() { + return Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 251, 251, 99).withOpacity(0.9), // Beige background + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(40), + topRight: Radius.circular(40), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.language, - size: 16, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.blue[200] - : Colors.blue[700], - ), - const SizedBox(width: 4), - Text( - _getChainDisplayName(walletProvider.currentChainName), - style: TextStyle( - fontSize: 12, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.blue[200] - : Colors.blue[700], - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 2), - Icon( - Icons.arrow_drop_down, - size: 16, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.blue[200] - : Colors.blue[700], - ), - ], + border: Border.all( + color: Colors.black.withOpacity(0.2), + width: 1, ), ), - tooltip: 'Switch Network', - onSelected: (chainId) async { - if (chainId != walletProvider.currentChainId) { - await _switchChain(context, walletProvider, chainId); - } - }, - itemBuilder: (BuildContext context) { - final supportedChains = walletProvider.getSupportedChains(); - return supportedChains.map((chain) { - final isCurrentChain = chain['isCurrentChain'] as bool; - return PopupMenuItem( - value: chain['chainId'] as String, - child: Row( - children: [ - Icon( - _getChainIcon(chain['chainId'] as String), - size: 20, - color: isCurrentChain - ? Colors.green - : Theme.of(context).iconTheme.color, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - chain['name'] as String, - style: TextStyle( - fontWeight: isCurrentChain - ? FontWeight.bold - : FontWeight.normal, - color: isCurrentChain - ? Colors.green - : null, - ), - ), - Text( - '${chain['nativeCurrency']['symbol']} Network', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).textTheme.bodySmall?.color, - ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final availableWidth = constraints.maxWidth; + final plantWidth = 35.0; + final plantSpacing = 0.0; + final totalPlantWidth = plantWidth + plantSpacing; + final visiblePlantCount = (availableWidth / totalPlantWidth).floor(); + + if (visiblePlantCount >= treeImages.length) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 2.5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: treeImages.map((imagePath) { + return Container( + width: plantWidth, + height: plantWidth, + child: Image.asset( + 'assets/tree-navbar-images/$imagePath', // Fixed: consistent path + width: 28, + height: 28, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.eco, + color: Colors.green[600], + size: 28, + ); + }, ), - ], - ), + ); + }).toList(), ), - if (isCurrentChain) - const Icon( - Icons.check_circle, - color: Colors.green, - size: 20, - ), - ], - ), - ); - }).toList(); - }, + ); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: treeImages.map((imagePath) { + return Container( + width: plantWidth, + height: plantWidth, + margin: EdgeInsets.zero, + child: Image.asset( + 'assets/tree-navbar-images/$imagePath', // Fixed: consistent path + width: 35, + height: 35, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.eco, + color: Colors.green[600], + size: 28, + ); + }, + ), + ); + }).toList(), + ), + ); + }, + ), + ), ); } Widget _buildWalletMenu(BuildContext context, WalletProvider walletProvider) { - return PopupMenuButton( - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: Theme.of(context).brightness == Brightness.dark - ? Colors.green[800] - : Colors.green[50], - label: Text( - formatAddress(walletProvider.currentAddress!), - style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.green[200] - : Colors.green[800], - ), - ), - avatar: Icon( - Icons.account_balance_wallet, - size: 20, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.green[200] - : Colors.green, - ), + return Container( + constraints: const BoxConstraints(maxWidth: 100), // Limit max width + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.green.withOpacity(0.3), + width: 1, ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - onSelected: (value) async { - if (value == 'disconnect') { - await walletProvider.disconnectWallet(); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Wallet disconnected'), - backgroundColor: Colors.green, + child: PopupMenuButton( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.account_balance_wallet, + size: 14, + color: Colors.green[700], ), - ); - } - } else if (value == 'copy') { - await Clipboard.setData( - ClipboardData(text: walletProvider.currentAddress!), - ); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Address copied to clipboard'), - backgroundColor: Colors.blue, + const SizedBox(width: 4), + Flexible( + child: Text( + formatAddress(walletProvider.currentAddress!), + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.w600, + fontSize: 10, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Icon( + Icons.arrow_drop_down, + size: 14, + color: Colors.green[700], ), + ], + ), + ), + onSelected: (value) async { + if (value == 'disconnect') { + await walletProvider.disconnectWallet(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Wallet disconnected'), + backgroundColor: Colors.green[600], + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } else if (value == 'copy') { + await Clipboard.setData( + ClipboardData(text: walletProvider.currentAddress!), ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Address copied to clipboard'), + backgroundColor: Colors.blue[600], + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } } - } - }, - itemBuilder: (BuildContext context) => [ - const PopupMenuItem( - value: 'copy', - child: ListTile( - leading: Icon(Icons.copy), - title: Text('Copy Address'), + }, + itemBuilder: (BuildContext context) => [ + const PopupMenuItem( + value: 'copy', + child: ListTile( + leading: Icon(Icons.copy, size: 20), + title: Text('Copy Address'), + contentPadding: EdgeInsets.symmetric(horizontal: 8), + ), ), - ), - const PopupMenuItem( - value: 'disconnect', - child: ListTile( - leading: Icon(Icons.logout, color: Colors.red), - title: Text( - 'Disconnect', - style: TextStyle(color: Colors.red), + const PopupMenuItem( + value: 'disconnect', + child: ListTile( + leading: Icon(Icons.logout, color: Colors.red, size: 20), + title: Text( + 'Disconnect', + style: TextStyle(color: Colors.red), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 8), ), ), - ), - ], + ], + ), ); } Widget _buildConnectButton(BuildContext context, WalletProvider walletProvider) { - return IconButton( - icon: const Icon(Icons.account_balance_wallet), - onPressed: () async { - final uri = await walletProvider.connectWallet(); - if (uri != null && context.mounted) { - showDialog( - context: context, - builder: (context) => WalletConnectDialog(uri: uri), - ); - } - }, - tooltip: 'Connect Wallet', - ); - } - - Future _switchChain(BuildContext context, WalletProvider walletProvider, String chainId) async { - try { - // Show loading indicator - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( + return Container( + constraints: const BoxConstraints(maxWidth: 80), // Limit max width + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.green.withOpacity(0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + final uri = await walletProvider.connectWallet(); + if (uri != null && context.mounted) { + showDialog( + context: context, + builder: (context) => WalletConnectDialog(uri: uri), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + Icon( + Icons.account_balance_wallet, + size: 16, + color: Colors.green[700], ), - const SizedBox(width: 16), - Expanded( - child: Text('Switching to ${walletProvider.chainInfo[chainId]?['name'] ?? 'Unknown Chain'}...'), + const SizedBox(width: 4), + Flexible( + child: Text( + 'Connect', + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.w600, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), ), ], ), - duration: const Duration(seconds: 5), - ), - ); - } - - // Perform chain switch - final success = await walletProvider.switchChain(chainId); - - if (context.mounted) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - - if (success) { - // Refresh chain info to make sure we have the latest - await walletProvider.refreshChainInfo(); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Switched to ${walletProvider.currentChainName}'), - backgroundColor: Colors.green, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Chain switch was cancelled'), - backgroundColor: Colors.orange, - ), - ); - } - } - } catch (e) { - print('Chain switch error: $e'); - if (context.mounted) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - - String errorMessage = 'Failed to switch chain'; - if (e.toString().contains('not supported')) { - errorMessage = 'Chain switching not supported by this wallet'; - } else if (e.toString().contains('User rejected')) { - errorMessage = 'Chain switch cancelled by user'; - } else if (e.toString().contains('4902')) { - errorMessage = 'Chain not found in wallet. Try adding it manually.'; - } - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(errorMessage), - backgroundColor: Colors.red, - action: SnackBarAction( - label: 'Retry', - onPressed: () => _switchChain(context, walletProvider, chainId), - ), ), - ); - } - } - } - - String _getChainDisplayName(String chainName) { - // Shorten chain names for display - switch (chainName) { - case 'Ethereum Mainnet': - return 'ETH'; - case 'Sepolia Testnet': - return 'SEP'; - case 'BNB Smart Chain': - return 'BSC'; - case 'Polygon': - return 'MATIC'; - case 'Avalanche C-Chain': - return 'AVAX'; - default: - return chainName.length > 6 ? chainName.substring(0, 6) : chainName; - } - } - - IconData _getChainIcon(String chainId) { - switch (chainId) { - case '1': - return Icons.diamond; // Ethereum - case '11155111': - return Icons.science; // Sepolia (testnet) - case '56': - return Icons.speed; // BSC - case '137': - return Icons.polyline; // Polygon - case '43114': - return Icons.ac_unit; // Avalanche - default: - return Icons.link; - } + ), + ), + ); } } \ No newline at end of file diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index 5319b35..d62a2d4 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -49,8 +49,9 @@ class _MintNftCoordinatesPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( - "This is the Mint NFT Coordinates page.", + "Enter your coordinates", style: TextStyle(fontSize: 30), + textAlign: TextAlign.center, ), const SizedBox(height: 20), TextField( @@ -74,7 +75,7 @@ class _MintNftCoordinatesPageState extends State { ElevatedButton( onPressed: submitCoordinates, child: const Text( - "->", + "Next", style: TextStyle(fontSize: 20, color: Colors.white), ), ) diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 6fd5e97..649010d 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -23,7 +23,7 @@ class _MintNftCoordinatesPageState extends State { if (description.isEmpty || species.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text("Please enter both latitude and longitude.")), + content: Text("Please enter both desriptiona and species.")), ); return; } @@ -43,14 +43,15 @@ class _MintNftCoordinatesPageState extends State { @override Widget build(BuildContext context) { return BaseScaffold( - title: "Mint NFT Coordinates", + title: "NFT Details", body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( - "This is the Mint NFT Coordinates page.", + "Enter NFT Details", style: TextStyle(fontSize: 30), + textAlign: TextAlign.center, ), const SizedBox(height: 20), TextField( @@ -58,7 +59,7 @@ class _MintNftCoordinatesPageState extends State { decoration: const InputDecoration( labelText: "Description", border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), + constraints: BoxConstraints(maxWidth: 300, maxHeight: 200), ), ), const SizedBox(height: 10), @@ -74,7 +75,7 @@ class _MintNftCoordinatesPageState extends State { ElevatedButton( onPressed: submitDetails, child: const Text( - "->", + "Next", style: TextStyle(fontSize: 20, color: Colors.white), ), ) diff --git a/pubspec.yaml b/pubspec.yaml index 8cb2d1b..44ab47a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,8 @@ flutter: # Add your .env file to assets assets: - .env + - assets/ + - assets/tree-navbar-images/ # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a From d6aaf1435dff2523563bfe0bf9964e8bcd6beab4 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Wed, 23 Jul 2025 14:17:46 +0530 Subject: [PATCH 06/18] add: universal logging --- lib/main.dart | 4 +++- lib/providers/wallet_provider.dart | 19 ++++++++++++------- lib/utils/logger.dart | 8 ++++++++ lib/utils/services/wallet_provider_utils.dart | 1 - pubspec.lock | 2 +- pubspec.yaml | 1 + 6 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 lib/utils/logger.dart diff --git a/lib/main.dart b/lib/main.dart index b2faf85..bf49649 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,16 +14,18 @@ import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; class NavigationService { static GlobalKey navigatorKey = GlobalKey(); } void main() async { + try { await dotenv.load(fileName: ".env"); } catch (e) { - print("No .env file found or error loading it: $e"); + logger.d("No .env file found or error loading it: $e"); } runApp(const MyApp()); diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 283a719..99c7028 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:tree_planting_protocol/models/wallet_option.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; enum InitializationState { notStarted, @@ -15,6 +16,7 @@ enum InitializationState { } class WalletProvider extends ChangeNotifier { + // ignore: deprecated_member_use Web3App? _web3App; String? _currentAddress; bool _isConnected = false; @@ -92,7 +94,7 @@ class WalletProvider extends ChangeNotifier { chainId: accountData[1], message: 'Connected successfully', ); - print('Session connected: ${event.session.topic}'); + ('Session connected: ${event.session.topic}'); } } } @@ -110,7 +112,7 @@ class WalletProvider extends ChangeNotifier { void _onSessionEvent(SessionEvent? event) { if (event != null) { - print('Session event: ${event.name}, data: ${event.data}'); + logger.d('Session event: ${event.name}, data: ${event.data}'); if (event.name == 'chainChanged') { final newChainId = event.data.toString(); @@ -120,7 +122,7 @@ class WalletProvider extends ChangeNotifier { if (_currentChainId != chainId) { _currentChainId = chainId; - _updateStatus('Chain changed to ${currentChainName}'); + _updateStatus('Chain changed to $currentChainName'); notifyListeners(); } } @@ -169,7 +171,9 @@ class WalletProvider extends ChangeNotifier { Future connectWallet() async { if (_initializationState != InitializationState.initialized || - _isConnecting) return null; + _isConnecting) { + return null; + } _updateStatus('Creating connection...'); _isConnecting = true; @@ -238,6 +242,7 @@ class WalletProvider extends ChangeNotifier { final Map> _chainInfo = chainInfoList; Map> get chainInfo => chainInfoList; + Future switchChain(String chainId) async { if (!_isConnected) { throw Exception('Wallet not connected'); @@ -366,13 +371,13 @@ class WalletProvider extends ChangeNotifier { if (_currentChainId != chainId) { _currentChainId = chainId; - _updateStatus('Chain updated to ${currentChainName}'); + _updateStatus('Chain updated to $currentChainName'); notifyListeners(); } } } } catch (e) { - print('Error refreshing chain info: $e'); + logger.e('Error refreshing chain info: $e'); } } @@ -409,7 +414,7 @@ class WalletProvider extends ChangeNotifier { return chainId; } } catch (e) { - print('Error getting current chain from wallet: $e'); + logger.e('Error getting current chain from wallet: $e'); } return _currentChainId; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart new file mode 100644 index 0000000..3c568ca --- /dev/null +++ b/lib/utils/logger.dart @@ -0,0 +1,8 @@ +import 'package:logger/logger.dart'; + +final logger = Logger( + printer: PrettyPrinter( + colors: true, + printEmojis: true, + ), +); diff --git a/lib/utils/services/wallet_provider_utils.dart b/lib/utils/services/wallet_provider_utils.dart index 6f5e95d..b2e75f7 100644 --- a/lib/utils/services/wallet_provider_utils.dart +++ b/lib/utils/services/wallet_provider_utils.dart @@ -3,7 +3,6 @@ Map? findFunctionInAbi( final functions = abi['functions'] ?? abi; if (functions is List) { - // Standard ABI format (array of function definitions) for (var func in functions) { if (func['name'] == functionName && func['type'] == 'function') { return func; diff --git a/pubspec.lock b/pubspec.lock index 3bb8eb1..ea51d66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -489,7 +489,7 @@ packages: source: hosted version: "5.1.1" logger: - dependency: transitive + dependency: "direct main" description: name: logger sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999" diff --git a/pubspec.yaml b/pubspec.yaml index 44ab47a..e278fe7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: cupertino_icons: ^1.0.8 flutter_dotenv: ^5.1.0 image_picker: ^1.0.4 + logger: ^2.0.2+1 dev_dependencies: flutter_test: From 3bd30eee3d6940c0958e2c09c2ad5ff56395e7e4 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Fri, 25 Jul 2025 20:35:42 +0530 Subject: [PATCH 07/18] fix: switch chain --- lib/main.dart | 17 +- ...t_option.dart => wallet_chain_option.dart} | 35 +- lib/pages/counter_page.dart | 304 +++++++++++++++-- lib/pages/mint_nft/mint_nft_coordinates.dart | 3 + lib/pages/mint_nft/mint_nft_details.dart | 6 + lib/pages/mint_nft/mint_nft_images.dart | 3 + lib/pages/switch_chain_page.dart | 193 +++++++++++ lib/providers/mint_nft_provider.dart | 1 + lib/providers/wallet_provider.dart | 317 ++++++++---------- lib/utils/constants/bottom_nav_constants.dart | 6 + lib/utils/services/counter_services.dart | 288 ++++++++++++++++ lib/utils/services/ipfs_services.dart | 1 - lib/widgets/tree_NFT_view_widget.dart | 55 +++ 13 files changed, 983 insertions(+), 246 deletions(-) rename lib/models/{wallet_option.dart => wallet_chain_option.dart} (70%) create mode 100644 lib/pages/switch_chain_page.dart create mode 100644 lib/utils/services/counter_services.dart create mode 100644 lib/widgets/tree_NFT_view_widget.dart diff --git a/lib/main.dart b/lib/main.dart index bf49649..38b51fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,8 +5,10 @@ import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_images.dart'; +import 'package:tree_planting_protocol/pages/switch_chain_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; +import 'package:tree_planting_protocol/pages/counter_page.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/providers/theme_provider.dart'; @@ -20,8 +22,8 @@ class NavigationService { static GlobalKey navigatorKey = GlobalKey(); } +final contractAddress = "0xa122109493B90e322824c3444ed8D6236CAbAB7C"; void main() async { - try { await dotenv.load(fileName: ".env"); } catch (e) { @@ -46,6 +48,13 @@ class MyApp extends StatelessWidget { return const HomePage(); }, ), + GoRoute( + path: '/counter', + name: 'counter_page', + builder: (BuildContext context, GoRouterState state) { + return CounterPage(); + }, + ), GoRoute( path: RouteConstants.mintNftPath, name: RouteConstants.mintNft, @@ -65,14 +74,12 @@ class MyApp extends StatelessWidget { return const MultipleImageUploadPage(); }, ), - ] - ), - + ]), GoRoute( path: RouteConstants.allTreesPath, name: RouteConstants.allTrees, builder: (BuildContext context, GoRouterState state) { - return const AllTreesPage(); + return const SwitchChainPage(); }, routes: [ GoRoute( diff --git a/lib/models/wallet_option.dart b/lib/models/wallet_chain_option.dart similarity index 70% rename from lib/models/wallet_option.dart rename to lib/models/wallet_chain_option.dart index 6cb380b..d0effe3 100644 --- a/lib/models/wallet_option.dart +++ b/lib/models/wallet_chain_option.dart @@ -59,7 +59,7 @@ class WalletOption { final Map> chainInfoList = { '1': { 'name': 'Ethereum Mainnet', - 'rpcUrl': 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY', + 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', 'nativeCurrency': { 'name': 'Ether', 'symbol': 'ETH', @@ -78,34 +78,7 @@ class WalletOption { }, 'blockExplorerUrl': 'https://sepolia.etherscan.io', }, - '56': { - 'name': 'BNB Smart Chain', - 'rpcUrl': 'https://bsc-dataseed.binance.org', - 'nativeCurrency': { - 'name': 'BNB', - 'symbol': 'BNB', - 'decimals': 18, - }, - 'blockExplorerUrl': 'https://bscscan.com', - }, - '137': { - 'name': 'Polygon', - 'rpcUrl': 'https://polygon-rpc.com', - 'nativeCurrency': { - 'name': 'MATIC', - 'symbol': 'MATIC', - 'decimals': 18, - }, - 'blockExplorerUrl': 'https://polygonscan.com', - }, - '43114': { - 'name': 'Avalanche C-Chain', - 'rpcUrl': 'https://api.avax.network/ext/bc/C/rpc', - 'nativeCurrency': { - 'name': 'AVAX', - 'symbol': 'AVAX', - 'decimals': 18, - }, - 'blockExplorerUrl': 'https://snowtrace.io', - }, }; + + + diff --git a/lib/pages/counter_page.dart b/lib/pages/counter_page.dart index 52bfda8..513cc3c 100644 --- a/lib/pages/counter_page.dart +++ b/lib/pages/counter_page.dart @@ -1,38 +1,294 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tree_planting_protocol/providers/counter_provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; -class CounterPage extends StatelessWidget { +class CounterPage extends StatefulWidget { const CounterPage({super.key}); + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + static const String contractAddress = '0xa122109493B90e322824c3444ed8D6236CAbAB7C'; + static const String chainId = '11155111'; // Sepolia testnet + + static const List> contractAbi = [ + { + "inputs": [], + "name": "getCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ]; + + String? currentCount; + bool isLoading = false; + String? errorMessage; + + @override + void initState() { + super.initState(); + _loadCount(); + } + + Future _loadCount() async { + setState(() { + isLoading = true; + errorMessage = null; + }); + + try { + final walletProvider = Provider.of(context, listen: false); + + final result = await walletProvider.readContract( + contractAddress: contractAddress, + functionName: 'getCount', + abi: contractAbi, + ); + + setState(() { + // The result is a List, and getCount returns a single uint256 + currentCount = result.isNotEmpty ? result[0].toString() : '0'; + isLoading = false; + }); + } catch (e) { + setState(() { + errorMessage = e.toString(); + isLoading = false; + }); + } + } + @override Widget build(BuildContext context) { return BaseScaffold( - title: "Counter Page", - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Contract Info Card + Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Contract Information', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + _buildInfoRow('Address:', contractAddress), + _buildInfoRow('Chain ID:', chainId), + _buildInfoRow('Network:', 'Sepolia Testnet'), + _buildInfoRow('Function:', 'getCount()'), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Count Display Card + Card( + elevation: 4, + color: Theme.of(context).primaryColor.withOpacity(0.1), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + const Text( + 'Current Count', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + if (isLoading) + const Column( + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading count...'), + ], + ) + else if (errorMessage != null) + Column( + children: [ + Icon( + Icons.error_outline, + size: 48, + color: Colors.red[400], + ), + const SizedBox(height: 8), + Text( + 'Error: $errorMessage', + style: TextStyle( + color: Colors.red[600], + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ) + else + Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).primaryColor, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + currentCount ?? '0', + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 8), + Text( + 'Last updated: ${DateTime.now().toString().substring(11, 19)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Refresh Button + ElevatedButton.icon( + onPressed: isLoading ? null : _loadCount, + icon: const Icon(Icons.refresh), + label: const Text('Refresh Count'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: const TextStyle(fontSize: 16), + ), + ), + + const Spacer(), + + // Status Information + Consumer( + builder: (context, walletProvider, child) { + return Card( + color: Colors.grey[50], + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Connection Status', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + walletProvider.isConnected + ? Icons.check_circle + : Icons.error_outline, + size: 16, + color: walletProvider.isConnected + ? Colors.green + : Colors.orange, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + walletProvider.isConnected + ? 'Wallet Connected' + : 'Wallet Not Connected (Reading via RPC)', + style: const TextStyle(fontSize: 12), + ), + ), + ], + ), + if (walletProvider.isConnected && walletProvider.currentChainId != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Current Chain: ${walletProvider.currentChainId}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "This is the counter page.", - style: TextStyle(fontSize: 30), + SizedBox( + width: 80, + child: Text( + label, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + Expanded( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontFamily: 'monospace', + ), + ), ), - const SizedBox(height: 20), - Consumer(builder: (ctx, _, __) { - return Text( - 'Counter: ${Provider.of(ctx, listen: true).getCount()}', - style: const TextStyle(fontSize: 24)); - }), - ElevatedButton( - onPressed: () { - Provider.of(context, listen: false).increment(); - }, - child: const Text("Increment Counter", - style: TextStyle(fontSize: 20, color: Colors.white)), - ) ], - )), + ), ); } -} +} \ No newline at end of file diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index d62a2d4..4214571 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; class MintNftCoordinatesPage extends StatefulWidget { const MintNftCoordinatesPage({super.key}); @@ -48,6 +49,8 @@ class _MintNftCoordinatesPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + const NewNFTWidget(), + const SizedBox(height: 20), const Text( "Enter your coordinates", style: TextStyle(fontSize: 30), diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 649010d..c419913 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; class MintNftDetailsPage extends StatefulWidget { const MintNftDetailsPage ({super.key}); @@ -48,6 +49,8 @@ class _MintNftCoordinatesPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + const NewNFTWidget(), + const SizedBox(height: 20), const Text( "Enter NFT Details", style: TextStyle(fontSize: 30), @@ -55,11 +58,14 @@ class _MintNftCoordinatesPageState extends State { ), const SizedBox(height: 20), TextField( + minLines: 4, + maxLines: 8, controller: descriptionController, decoration: const InputDecoration( labelText: "Description", border: OutlineInputBorder(), constraints: BoxConstraints(maxWidth: 300, maxHeight: 200), + alignLabelWithHint: true, ), ), const SizedBox(height: 10), diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart index 5681ad3..eaf94da 100644 --- a/lib/pages/mint_nft/mint_nft_images.dart +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; class MultipleImageUploadPage extends StatefulWidget { const MultipleImageUploadPage({Key? key}) : super(key: key); @@ -163,6 +164,8 @@ class _MultipleImageUploadPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const NewNFTWidget(), + const SizedBox(height: 20), Row( children: [ Expanded( diff --git a/lib/pages/switch_chain_page.dart b/lib/pages/switch_chain_page.dart new file mode 100644 index 0000000..a2f3ea7 --- /dev/null +++ b/lib/pages/switch_chain_page.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; + +class SwitchChainPage extends StatelessWidget { + const SwitchChainPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BaseScaffold( + body: Consumer( + builder: (context, walletProvider, child) { + return Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + children: [ + const Text( + 'Current Chain', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + Text( + walletProvider.isConnected + ? '${walletProvider.currentChainName} (${walletProvider.currentChainId})' + : 'Not Connected', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue[200]!), + ), + child: Text( + walletProvider.statusMessage, + style: TextStyle( + color: Colors.blue[800], + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: walletProvider.isConnected + ? () => _showChainSelector(context, walletProvider) + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey[300], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: const Text( + 'Switch Chain', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + if (!walletProvider.isConnected) ...[ + const SizedBox(height: 16), + Text( + 'Please connect your wallet first', + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ], + ), + ), + ); + }, + ), + ); + } + + void _showChainSelector(BuildContext context, WalletProvider walletProvider) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Select Chain', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ...walletProvider.getSupportedChains().map((chain) { + final isCurrentChain = chain['isCurrentChain'] as bool; + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + title: Text( + chain['name'] as String, + style: TextStyle( + fontWeight: isCurrentChain ? FontWeight.bold : FontWeight.normal, + ), + ), + subtitle: Text('Chain ID: ${chain['chainId']}'), + trailing: isCurrentChain + ? const Icon(Icons.check_circle, color: Colors.green) + : const Icon(Icons.arrow_forward_ios), + tileColor: isCurrentChain ? Colors.green[50] : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: isCurrentChain ? Colors.green : Colors.grey[300]!, + ), + ), + onTap: isCurrentChain ? null : () async { + Navigator.pop(context); + await _switchToChain(context, walletProvider, chain['chainId'] as String); + }, + ), + ); + }).toList(), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } + + Future _switchToChain(BuildContext context, WalletProvider walletProvider, String chainId) async { + try { + final success = await walletProvider.switchChain(chainId); + if (success && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Chain switched successfully!'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to switch chain: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } +} \ No newline at end of file diff --git a/lib/providers/mint_nft_provider.dart b/lib/providers/mint_nft_provider.dart index b6c5968..6d83765 100644 --- a/lib/providers/mint_nft_provider.dart +++ b/lib/providers/mint_nft_provider.dart @@ -16,6 +16,7 @@ class MintNftProvider extends ChangeNotifier { String getImageUri() => _imageUri; String getQrIpfsHash() => _qrIpfsHash; String getGeoHash() => _geoHash; + String getDescription() => _description; List getInitialPhotos() => _initialPhotos; void setLatitude(double latitude) { diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 99c7028..28dc37f 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -2,12 +2,18 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + import 'package:url_launcher/url_launcher.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; -import 'package:tree_planting_protocol/models/wallet_option.dart'; +import 'package:tree_planting_protocol/models/wallet_chain_option.dart'; + import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; +import 'dart:convert'; +import 'package:web3dart/web3dart.dart'; +import 'package:http/http.dart' as http; + enum InitializationState { notStarted, initializing, @@ -43,6 +49,7 @@ class WalletProvider extends ChangeNotifier { try { _updateStatus('Initializing Web3App...'); + // ignore: deprecated_member_use _web3App = await Web3App.createInstance( projectId: dotenv.env['WALLETCONNECT_PROJECT_ID'] ?? '', metadata: const PairingMetadata( @@ -50,6 +57,10 @@ class WalletProvider extends ChangeNotifier { description: 'Tokenise Tree plantations on blockchain', url: 'https://walletconnect.com/', icons: ['https://walletconnect.com/walletconnect-logo.png'], + redirect: Redirect( + native: 'treeplantingprotocol://', + universal: 'https://treeplantingprotocol.com/callback', + ), ), ); @@ -183,7 +194,7 @@ class WalletProvider extends ChangeNotifier { final ConnectResponse connectResponse = await _web3App!.connect( requiredNamespaces: { 'eip155': const RequiredNamespace( - chains: ['eip155:11155111'], // Sepolia and mainnet + chains: ['eip155:11155111', 'eip155:1'], methods: [ 'eth_sendTransaction', 'eth_signTransaction', @@ -234,192 +245,9 @@ class WalletProvider extends ChangeNotifier { return null; } - Future switchToSepolia() async { - return await switchChain(_defaultChainId); - } - - bool get isConnectedToSepolia => _currentChainId == _defaultChainId; - final Map> _chainInfo = chainInfoList; Map> get chainInfo => chainInfoList; - Future switchChain(String chainId) async { - if (!_isConnected) { - throw Exception('Wallet not connected'); - } - - if (_currentChainId == chainId) { - return true; - } - - if (!_chainInfo.containsKey(chainId)) { - throw Exception('Unsupported chain ID: $chainId'); - } - - try { - _updateStatus('Switching to ${_chainInfo[chainId]!['name']}...'); - - final sessions = _web3App!.sessions.getAll(); - if (sessions.isEmpty) { - throw Exception('No active session found'); - } - - final session = sessions.first; - final supportedMethods = session.namespaces['eip155']?.methods ?? []; - - if (!supportedMethods.contains('wallet_switchEthereumChain')) { - throw Exception('Chain switching not supported by this wallet'); - } - - final hexChainId = '0x${int.parse(chainId).toRadixString(16)}'; - final currentSessionChainId = _getCurrentSessionChainId(session); - - await _web3App!.request( - topic: session.topic, - chainId: 'eip155:$currentSessionChainId', - request: SessionRequestParams( - method: 'wallet_switchEthereumChain', - params: [ - { - 'chainId': hexChainId, - } - ], - ), - ); - await _waitForChainChange(chainId); - _updateStatus('Switched to ${_chainInfo[chainId]!['name']}'); - return true; - } catch (e) { - String errorString = e.toString().toLowerCase(); - if (errorString.contains('4001') || - errorString.contains('user rejected') || - errorString.contains('user denied') || - errorString.contains('cancelled')) { - _updateStatus('Chain switch cancelled by user'); - return false; - } - if (errorString.contains('4902') || - errorString.contains('unrecognized chain') || - errorString.contains('chain not found') || - errorString.contains('unknown chain')) { - _updateStatus('Chain not found in wallet - please add it manually'); - throw Exception( - 'Chain not found in wallet. Please add ${_chainInfo[chainId]!['name']} manually in your wallet.'); - } - - _updateStatus('Failed to switch chain: ${e.toString()}'); - throw Exception('Failed to switch chain: $e'); - } - } - - String _getCurrentSessionChainId(SessionData session) { - final accounts = session.namespaces['eip155']?.accounts; - if (accounts != null && accounts.isNotEmpty) { - final accountData = accounts.first.split(':'); - return accountData[1]; - } - return _currentChainId ?? '1'; - } - - Future _waitForChainChange(String expectedChainId, - {Duration timeout = const Duration(seconds: 10)}) async { - final completer = Completer(); - bool isListening = true; - - void handleChainChange(SessionEvent? event) { - if (!isListening) return; - - if (event?.name == 'chainChanged') { - final newChainId = event!.data.toString(); - final chainId = newChainId.startsWith('0x') - ? int.parse(newChainId.substring(2), radix: 16).toString() - : newChainId; - - if (chainId == expectedChainId) { - isListening = false; - _web3App!.onSessionEvent.unsubscribe(handleChainChange); - completer.complete(); - } - } - } - - _web3App!.onSessionEvent.subscribe(handleChainChange); - - Timer(timeout, () { - if (!completer.isCompleted) { - isListening = false; - _web3App!.onSessionEvent.unsubscribe(handleChainChange); - completer.complete(); - } - }); - - await completer.future; - } - - Future refreshChainInfo() async { - if (!_isConnected) return; - - try { - final sessions = _web3App!.sessions.getAll(); - if (sessions.isNotEmpty) { - final session = sessions.first; - final accounts = session.namespaces['eip155']?.accounts; - - if (accounts != null && accounts.isNotEmpty) { - final accountData = accounts.first.split(':'); - final chainId = accountData[1]; - - if (_currentChainId != chainId) { - _currentChainId = chainId; - _updateStatus('Chain updated to $currentChainName'); - notifyListeners(); - } - } - } - } catch (e) { - logger.e('Error refreshing chain info: $e'); - } - } - - Future getCurrentChainFromWallet() async { - if (!_isConnected) return null; - - try { - final sessions = _web3App!.sessions.getAll(); - if (sessions.isEmpty) return null; - - final session = sessions.first; - final currentSessionChainId = _getCurrentSessionChainId(session); - - final result = await _web3App!.request( - topic: session.topic, - chainId: 'eip155:$currentSessionChainId', - request: SessionRequestParams( - method: 'eth_chainId', - params: [], - ), - ); - - if (result != null) { - final chainIdHex = result.toString(); - final chainId = chainIdHex.startsWith('0x') - ? int.parse(chainIdHex.substring(2), radix: 16).toString() - : chainIdHex; - - if (_currentChainId != chainId) { - _currentChainId = chainId; - notifyListeners(); - } - - return chainId; - } - } catch (e) { - logger.e('Error getting current chain from wallet: $e'); - } - - return _currentChainId; - } - List> getSupportedChains() { return _chainInfo.entries.map((entry) { return { @@ -431,6 +259,21 @@ class WalletProvider extends ChangeNotifier { }).toList(); } + List> getChainDetails(String chainId) { + if (!_chainInfo.containsKey(chainId)) { + throw Exception('Unsupported chain ID: $chainId'); + } + final chain = _chainInfo[chainId]!; + return [ + { + 'name': chain['name'], + 'rpcUrl': chain['rpcUrl'], + 'nativeCurrency': chain['nativeCurrency'], + 'isCurrentChain': chainId == _currentChainId, + }, + ]; + } + bool isChainSupported(String chainId) { return _chainInfo.containsKey(chainId); } @@ -509,6 +352,110 @@ class WalletProvider extends ChangeNotifier { } } + void getSupportedChainsWithStatus() async { + if (!_isConnected) { + throw Exception('Wallet not connected'); + } + } + + String _getCurrentSessionChainId() { + final sessions = _web3App!.sessions.getAll(); + if (!sessions.isNotEmpty) { + throw Exception('No active WalletConnect session'); + } + final accounts = sessions.first.namespaces['eip155']?.accounts; + if (accounts != null && accounts.isNotEmpty) { + return accounts.first.split(':')[1]; + } + return '11155111'; // Default to Sepolia if no accounts found + } + + Future switchChain(String newChainId) async { + logger.d('[switchChain] Requested chain id: $newChainId'); + if (!_isConnected) { + print('[switchChain] Wallet not connected.'); + throw Exception('Wallet not connected'); + } + + if (_currentChainId == newChainId) { + logger.d('[switchChain] Already on chain $newChainId, skipping switch.'); + return true; + } + _updateStatus('Switching to ${chainInfo['name']}...'); + _currentChainId = newChainId; + notifyListeners(); + return true; + } + + + + + + + + Future readContract({ + required String contractAddress, + required String functionName, + required dynamic abi, + List params = const [], + }) async { + try { + if (!_isConnected || _web3App == null || _currentChainId == null) { + throw Exception('Wallet not connected'); + } + _updateStatus('Reading from contract...'); + List abiList; + if (abi is String) { + abiList = json.decode(abi); + } else if (abi is List) { + abiList = abi; + } else { + throw Exception('Invalid ABI format'); + } + final contract = DeployedContract( + ContractAbi.fromJson(json.encode(abiList), ''), + EthereumAddress.fromHex(contractAddress), + ); + final function = contract.function(functionName); + final targetChainId = _currentChainId ?? _defaultChainId; + final rpcUrl = getChainDetails(targetChainId).first['rpcUrl'] as String?; + final httpClient = http.Client(); + final ethClient = Web3Client(rpcUrl!, httpClient); + final result = await ethClient.call( + contract: contract, + function: function, + params: params, + ); + + httpClient.close(); + _updateStatus('Contract read successful'); + + return result; + } catch (e) { + _updateStatus('Contract read failed: ${e.toString()}'); + logger.e('Error reading contract: $e'); + throw Exception('Failed to read contract: $e'); + } + } + + + + + + + + + + + + + + + + + + + Future disconnectWallet() async { if (!_isConnected) return; diff --git a/lib/utils/constants/bottom_nav_constants.dart b/lib/utils/constants/bottom_nav_constants.dart index a77a094..bb7d587 100644 --- a/lib/utils/constants/bottom_nav_constants.dart +++ b/lib/utils/constants/bottom_nav_constants.dart @@ -35,5 +35,11 @@ class BottomNavConstants { activeIcon: Icons.nature_people, route: RouteConstants.mintNftPath, ), + BottomNavItem( + label: 'Counter', + icon: Icons.nature_people_outlined, + activeIcon: Icons.nature_people, + route: '/counter', + ), ]; } \ No newline at end of file diff --git a/lib/utils/services/counter_services.dart b/lib/utils/services/counter_services.dart new file mode 100644 index 0000000..3a94ea2 --- /dev/null +++ b/lib/utils/services/counter_services.dart @@ -0,0 +1,288 @@ +// import 'package:web3dart/web3dart.dart'; +// import 'package:tree_planting_protocol/providers/wallet_provider.dart'; + +// class CounterService { +// // Sepolia testnet chain ID +// static const int SEPOLIA_CHAIN_ID = 11155111; + +// // ABI for a simple counter contract +// static const String counterABI = ''' +// [ +// { +// "inputs": [], +// "name": "count", +// "outputs": [ +// { +// "internalType": "uint256", +// "name": "", +// "type": "uint256" +// } +// ], +// "stateMutability": "view", +// "type": "function" +// }, +// { +// "inputs": [], +// "name": "increment", +// "outputs": [], +// "stateMutability": "nonpayable", +// "type": "function" +// }, +// { +// "inputs": [], +// "name": "getCount", +// "outputs": [ +// { +// "internalType": "uint256", +// "name": "", +// "type": "uint256" +// } +// ], +// "stateMutability": "view", +// "type": "function" +// } +// ] +// '''; + +// /// Get the current count from the smart contract +// static Future getCount( +// WalletProvider walletProvider, +// String contractAddress, +// ) async { +// try { +// // Ensure we have a web3 client +// if (walletProvider.ethClient == null) { +// throw Exception('Web3Client not initialized'); +// } + +// // Verify we're on the correct chain (Sepolia) +// await _verifyChainId(walletProvider); + +// // Create contract instance +// final contract = DeployedContract( +// ContractAbi.fromJson(counterABI, 'Counter'), +// EthereumAddress.fromHex(contractAddress), +// ); + +// // Get the count function +// final countFunction = contract.function('getCount') ?? +// contract.function('count'); + +// if (countFunction == null) { +// throw Exception('Count function not found in contract'); +// } + +// // Call the contract function +// final result = await walletProvider.ethClient!.call( +// contract: contract, +// function: countFunction, +// params: [], +// ); + +// // Return the count value +// if (result.isNotEmpty && result[0] is BigInt) { +// return result[0] as BigInt; +// } else { +// throw Exception('Invalid response from contract'); +// } +// } catch (e) { +// throw Exception('Failed to get count: ${e.toString()}'); +// } +// } + +// /// Increment the counter by calling the smart contract +// static Future increment( +// WalletProvider walletProvider, +// String contractAddress, +// ) async { +// try { +// // Ensure wallet is connected +// if (!walletProvider.isConnected || walletProvider.currentAddress!.isEmpty) { +// throw Exception('Wallet not connected'); +// } + +// // Ensure we're on Sepolia +// await _verifyChainId(walletProvider); + +// // Ensure we have a web3 client +// if (walletProvider.ethClient == null) { +// throw Exception('Web3Client not initialized'); +// } + +// // Create contract instance +// final contract = DeployedContract( +// ContractAbi.fromJson(counterABI, 'Counter'), +// EthereumAddress.fromHex(contractAddress), +// ); + +// // Get the increment function +// final incrementFunction = contract.function('increment'); +// if (incrementFunction == null) { +// throw Exception('Increment function not found in contract'); +// } + +// // Get current gas price +// final gasPrice = await walletProvider.ethClient!.getGasPrice(); + +// // Estimate gas for the transaction +// BigInt gasLimit; +// try { +// gasLimit = await walletProvider.ethClient!.estimateGas( +// sender: EthereumAddress.fromHex(walletProvider.currentAddress!), +// to: EthereumAddress.fromHex(contractAddress), +// data: incrementFunction.encodeCall([]), +// ); +// // Add 20% buffer to gas limit +// gasLimit = BigInt.from((gasLimit.toDouble() * 1.2).round()); +// } catch (e) { +// // Fallback gas limit if estimation fails +// gasLimit = BigInt.from(100000); +// } + +// // Create the transaction +// final transaction = Transaction.callContract( +// contract: contract, +// function: incrementFunction, +// parameters: [], +// from: EthereumAddress.fromHex(walletProvider.currentAddress!), +// gasPrice: gasPrice, +// maxGas: gasLimit.toInt(), +// ); + +// // Send the transaction +// final txHash = await walletProvider.sendTransaction(transaction +// , contractAddress: contractAddress, +// functionName: 'increment', +// parameters: [], +// ); + +// return txHash; +// } catch (e) { +// throw Exception('Failed to increment: ${e.toString()}'); +// } +// } + +// /// Get transaction receipt and status +// static Future getTransactionReceipt( +// WalletProvider walletProvider, +// String txHash, +// ) async { +// try { +// if (walletProvider.ethClient == null) { +// throw Exception('Web3Client not initialized'); +// } + +// return await walletProvider.ethClient!.getTransactionReceipt(txHash); +// } catch (e) { +// throw Exception('Failed to get transaction receipt: ${e.toString()}'); +// } +// } + +// /// Wait for transaction confirmation +// static Future waitForTransaction( +// WalletProvider walletProvider, +// String txHash, { +// Duration timeout = const Duration(minutes: 5), +// Duration pollInterval = const Duration(seconds: 2), +// }) async { +// final startTime = DateTime.now(); + +// while (DateTime.now().difference(startTime) < timeout) { +// try { +// final receipt = await getTransactionReceipt(walletProvider, txHash); +// if (receipt != null) { +// return receipt; +// } +// } catch (e) { +// // Continue polling on error +// } + +// await Future.delayed(pollInterval); +// } + +// throw Exception('Transaction confirmation timeout'); +// } + +// /// Get the Sepolia Etherscan URL for a transaction +// static String getEtherscanUrl(String txHash) { +// return 'https://sepolia.etherscan.io/tx/$txHash'; +// } + +// /// Validate contract address format +// static bool isValidContractAddress(String address) { +// try { +// EthereumAddress.fromHex(address); +// return true; +// } catch (e) { +// return false; +// } +// } + +// /// Format BigInt count for display +// static String formatCount(BigInt count) { +// return count.toString(); +// } + +// /// Convert Wei to Ether for gas calculations +// static String weiToEther(BigInt wei) { +// return EtherAmount.inWei(wei).getValueInUnit(EtherUnit.ether).toString(); +// } + +// /// Verify that we're connected to the Sepolia testnet +// static Future _verifyChainId(WalletProvider walletProvider) async { +// try { +// final chainId = await walletProvider.ethClient!.getChainId(); +// if (chainId != SEPOLIA_CHAIN_ID) { +// throw Exception( +// 'Wrong network! Please switch to Sepolia testnet (Chain ID: $SEPOLIA_CHAIN_ID). ' +// 'Current Chain ID: $chainId' +// ); +// } +// } catch (e) { +// if (e.toString().contains('Wrong network')) { +// rethrow; +// } +// throw Exception('Failed to verify chain ID: ${e.toString()}'); +// } +// } + +// /// Get current chain ID +// static Future getCurrentChainId(WalletProvider walletProvider) async { +// try { +// if (walletProvider.ethClient == null) { +// throw Exception('Web3Client not initialized'); +// } +// return await walletProvider.ethClient!.getChainId(); +// } catch (e) { +// throw Exception('Failed to get chain ID: ${e.toString()}'); +// } +// } + +// /// Check if currently connected to Sepolia +// static Future isConnectedToSepolia(WalletProvider walletProvider) async { +// try { +// final chainId = await getCurrentChainId(walletProvider); +// return chainId == SEPOLIA_CHAIN_ID; +// } catch (e) { +// return false; +// } +// } + +// /// Get chain name from chain ID +// static String getChainName(int chainId) { +// switch (chainId) { +// case 1: +// return 'Ethereum Mainnet'; +// case 5: +// return 'Goerli Testnet'; +// case 11155111: +// return 'Sepolia Testnet'; +// case 137: +// return 'Polygon Mainnet'; +// case 80001: +// return 'Polygon Mumbai'; +// default: +// return 'Unknown Network (ID: $chainId)'; +// } +// } +// } \ No newline at end of file diff --git a/lib/utils/services/ipfs_services.dart b/lib/utils/services/ipfs_services.dart index 975d3e1..89b3966 100644 --- a/lib/utils/services/ipfs_services.dart +++ b/lib/utils/services/ipfs_services.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; diff --git a/lib/widgets/tree_NFT_view_widget.dart b/lib/widgets/tree_NFT_view_widget.dart new file mode 100644 index 0000000..ea8c96b --- /dev/null +++ b/lib/widgets/tree_NFT_view_widget.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/services/wallet_provider_utils.dart'; + +class NewNFTWidget extends StatefulWidget { + const NewNFTWidget({super.key}); + + @override + State createState() => _NewNFTWidgetState(); +} + + +String _formatDescription(String description) { + return description.length > 50 + ? '${description.substring(0, 50)}...' + : description; +} +class _NewNFTWidgetState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.green, width: 2), + borderRadius: BorderRadius.circular(8.0), + color: Colors.white, + ), + child: Center( + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Consumer(builder: (ctx, _, __) { + return Column(children: [ + Text( + 'Latitude: ${Provider.of(ctx, listen: true).getLatitude()}', + style: const TextStyle(fontSize: 20), + ), + Text( + 'Longitude: ${Provider.of(ctx, listen: true).getLongitude()}', + style: const TextStyle(fontSize: 20), + ), + Text( + 'Species: ${Provider.of(ctx, listen: true).getSpecies()}', + style: const TextStyle(fontSize: 20), + ), + Text( + 'Description: ${formatAddress(Provider.of(ctx, listen: true).getDescription())}', + style: const TextStyle(fontSize: 20), + ), + ]); + }) + ]), + ), + ); + } +} From 769d60280b7f72bf12ec90e87d6a1847004ecc70 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 26 Jul 2025 00:17:45 +0530 Subject: [PATCH 08/18] feat: add write contract functionality --- lib/pages/counter_page.dart | 256 +++++++++++++++++++++++++++-- lib/providers/wallet_provider.dart | 224 ++++++++++++++++++++++++- 2 files changed, 457 insertions(+), 23 deletions(-) diff --git a/lib/pages/counter_page.dart b/lib/pages/counter_page.dart index 513cc3c..690a543 100644 --- a/lib/pages/counter_page.dart +++ b/lib/pages/counter_page.dart @@ -27,12 +27,21 @@ class _CounterPageState extends State { ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "increment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ]; String? currentCount; bool isLoading = false; + bool isIncrementing = false; String? errorMessage; + String? lastTransactionHash; @override void initState() { @@ -68,9 +77,102 @@ class _CounterPageState extends State { } } + Future _incrementCount() async { + final walletProvider = Provider.of(context, listen: false); + + if (!walletProvider.isConnected) { + _showErrorDialog('Wallet Not Connected', + 'Please connect your wallet to increment the counter.'); + return; + } + + if (walletProvider.currentChainId != chainId) { + _showErrorDialog('Wrong Network', + 'Please switch to Sepolia testnet (Chain ID: $chainId) to interact with this contract.'); + return; + } + + setState(() { + isIncrementing = true; + }); + + try { + final txHash = await walletProvider.writeContract( + contractAddress: contractAddress, + functionName: 'increment', + abi: contractAbi, + chainId: chainId, + ); + + setState(() { + lastTransactionHash = txHash; + isIncrementing = false; + }); + + _showSuccessDialog('Transaction Sent!', + 'Transaction hash: ${txHash.substring(0, 10)}...\n\nThe counter will update once the transaction is confirmed.'); + + // Auto-refresh count after a delay to allow transaction confirmation + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + _loadCount(); + } + }); + + } catch (e) { + setState(() { + isIncrementing = false; + }); + _showErrorDialog('Transaction Failed', e.toString()); + } + } + + void _showErrorDialog(String title, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + void _showSuccessDialog(String title, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.green), + const SizedBox(width: 8), + Text(title), + ], + ), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return BaseScaffold( + title: 'Counter', body: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -186,17 +288,100 @@ class _CounterPageState extends State { const SizedBox(height: 24), - // Refresh Button - ElevatedButton.icon( - onPressed: isLoading ? null : _loadCount, - icon: const Icon(Icons.refresh), - label: const Text('Refresh Count'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - textStyle: const TextStyle(fontSize: 16), - ), + // Action Buttons + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: isLoading ? null : _loadCount, + icon: const Icon(Icons.refresh), + label: const Text('Refresh Count'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: const TextStyle(fontSize: 16), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Consumer( + builder: (context, walletProvider, child) { + final bool canIncrement = walletProvider.isConnected && + walletProvider.currentChainId == chainId && + !isIncrementing && !isLoading; + + return ElevatedButton.icon( + onPressed: canIncrement ? _incrementCount : null, + icon: isIncrementing + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.add), + label: Text(isIncrementing ? 'Incrementing...' : 'Increment'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: const TextStyle(fontSize: 16), + ), + ); + }, + ), + ), + ], ), + // Transaction Hash Display + if (lastTransactionHash != null) ...[ + const SizedBox(height: 16), + Card( + color: Colors.green[50], + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.check_circle, color: Colors.green, size: 16), + SizedBox(width: 8), + Text( + 'Last Transaction', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Hash: ${lastTransactionHash!.substring(0, 20)}...', + style: const TextStyle( + fontSize: 12, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 4), + Text( + 'View on Etherscan', + style: TextStyle( + fontSize: 12, + color: Colors.blue[600], + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + ), + ], + const Spacer(), // Status Information @@ -233,23 +418,64 @@ class _CounterPageState extends State { child: Text( walletProvider.isConnected ? 'Wallet Connected' - : 'Wallet Not Connected (Reading via RPC)', + : 'Wallet Not Connected (Read-only mode)', style: const TextStyle(fontSize: 12), ), ), ], ), - if (walletProvider.isConnected && walletProvider.currentChainId != null) - Padding( - padding: const EdgeInsets.only(top: 4), + if (walletProvider.isConnected) ...[ + if (walletProvider.currentChainId != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Icon( + walletProvider.currentChainId == chainId + ? Icons.check_circle + : Icons.warning, + size: 14, + color: walletProvider.currentChainId == chainId + ? Colors.green + : Colors.orange, + ), + const SizedBox(width: 6), + Text( + 'Chain: ${walletProvider.currentChainId} ${walletProvider.currentChainId == chainId ? '(Correct)' : '(Switch to $chainId)'}', + style: TextStyle( + fontSize: 12, + color: walletProvider.currentChainId == chainId + ? Colors.green[700] + : Colors.orange[700], + ), + ), + ], + ), + ), + if (walletProvider.currentAddress != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + 'Address: ${walletProvider.currentAddress!.substring(0, 6)}...${walletProvider.currentAddress!.substring(walletProvider.currentAddress!.length - 4)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontFamily: 'monospace', + ), + ), + ), + ] else ...[ + const Padding( + padding: EdgeInsets.only(top: 4), child: Text( - 'Current Chain: ${walletProvider.currentChainId}', + 'Connect wallet to increment counter', style: TextStyle( fontSize: 12, - color: Colors.grey[600], + fontStyle: FontStyle.italic, ), ), ), + ], ], ), ), diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 28dc37f..970a866 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -387,12 +387,6 @@ class WalletProvider extends ChangeNotifier { return true; } - - - - - - Future readContract({ required String contractAddress, required String functionName, @@ -438,23 +432,237 @@ class WalletProvider extends ChangeNotifier { } } + Future writeContract({ + required String contractAddress, + required String functionName, + required dynamic abi, + String? chainId, + List params = const [], + BigInt? value, + BigInt? gasLimit, + }) async { + try { + if (!_isConnected || _web3App == null || _currentAddress == null) { + throw Exception('Wallet not connected'); + } + _updateStatus('Preparing transaction...'); + List abiList; + if (abi is String) { + abiList = json.decode(abi); + } else if (abi is List) { + abiList = abi; + } else { + throw Exception('Invalid ABI format'); + } + final contract = DeployedContract( + ContractAbi.fromJson(json.encode(abiList), ''), + EthereumAddress.fromHex(contractAddress), + ); + final function = contract.function(functionName); + final encodedFunction = function.encodeCall(params); + final targetChainId = chainId ?? _currentChainId ?? _defaultChainId; + if (_currentChainId != targetChainId) { + logger.w( + 'Target chain ($targetChainId) differs from current chain ($_currentChainId)'); + _updateStatus( + 'Chain mismatch detected. Current: $_currentChainId, Target: $targetChainId'); + } + final rpcUrl = getChainDetails(targetChainId).first['rpcUrl'] as String?; + final httpClient = http.Client(); + final ethClient = Web3Client(rpcUrl as String, httpClient); + final nonce = await ethClient.getTransactionCount( + EthereumAddress.fromHex(_currentAddress!), + ); + BigInt estimatedGas = gasLimit ?? BigInt.from(100000); + if (gasLimit == null) { + try { + estimatedGas = await ethClient.estimateGas( + sender: EthereumAddress.fromHex(_currentAddress!), + to: EthereumAddress.fromHex(contractAddress), + data: encodedFunction, + value: value != null ? EtherAmount.inWei(value) : null, + ); + estimatedGas = (estimatedGas * BigInt.from(120)) ~/ BigInt.from(100); + } catch (e) { + logger.w('Gas estimation failed, using default: $e'); + } + } + final gasPrice = await ethClient.getGasPrice(); + httpClient.close(); + final transaction = { + 'from': _currentAddress!, + 'to': contractAddress, + 'data': + '0x${encodedFunction.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join()}', + 'gas': '0x${estimatedGas.toRadixString(16)}', + 'gasPrice': '0x${gasPrice.getInWei.toRadixString(16)}', + 'nonce': '0x${nonce.toRadixString(16)}', + }; + if (value != null && value > BigInt.zero) { + transaction['value'] = '0x${value.toRadixString(16)}'; + } + _updateStatus('Opening wallet for transaction approval...'); + final sessions = _web3App!.sessions.getAll(); + if (sessions.isEmpty) { + throw Exception('No active WalletConnect session'); + } + final session = sessions.first; + final requestParams = SessionRequestParams( + method: 'eth_sendTransaction', + params: [transaction], + ); + final requestFuture = _web3App!.request( + topic: session.topic, + chainId: 'eip155:$targetChainId', + request: requestParams, + ); + await _openConnectedWalletForTransaction(session); + _updateStatus('Waiting for transaction approval in wallet...'); + final result = await requestFuture.timeout( + const Duration(minutes: 5), + onTimeout: () { + throw Exception('Transaction approval timeout - please try again'); + }, + ); + final txHash = result.toString(); + _updateStatus('Transaction sent: ${txHash.substring(0, 10)}...'); + logger.i('Transaction hash: $txHash'); + return txHash; + } catch (e) { + _updateStatus('Transaction failed: ${e.toString()}'); + logger.e('Error writing to contract: $e'); + throw Exception('Failed to write to contract: $e'); + } + } + Future _openConnectedWalletForTransaction(SessionData session) async { + try { + final peerMetadata = session.peer.metadata; + final walletName = peerMetadata.name.toLowerCase(); + WalletOption? matchedWallet; + + for (final wallet in walletOptionsList) { + final walletNameLower = wallet.name.toLowerCase(); + if (walletName.contains(walletNameLower) || + walletNameLower + .contains(walletName.split(' ').first.toLowerCase())) { + matchedWallet = wallet; + break; + } + } + if (matchedWallet != null) { + logger.d('Opening ${matchedWallet.name} for transaction approval'); + await _openWalletAppForTransaction(matchedWallet); + } else { + logger.d('Unknown wallet: $walletName, trying generic approach'); + await _openGenericWallet(walletName); + } + } catch (e) { + logger.w('Failed to auto-open wallet app: $e'); + } + } + Future _openWalletAppForTransaction(WalletOption wallet) async { + try { + String deepLinkBase = wallet.deepLink.replaceAll('wc?uri=', ''); + if (!deepLinkBase.endsWith('://')) { + deepLinkBase = deepLinkBase.replaceAll('://', '://'); + } + final Uri deepLink = Uri.parse('${deepLinkBase}wc'); + bool launched = false; + if (await canLaunchUrl(deepLink)) { + launched = await launchUrl( + deepLink, + mode: LaunchMode.externalApplication, + ); + } + if (!launched && wallet.fallbackUrl != null) { + String fallbackBase = wallet.fallbackUrl!.replaceAll('wc?uri=', ''); + final Uri fallbackUri = Uri.parse('${fallbackBase}wc'); + if (await canLaunchUrl(fallbackUri)) { + await launchUrl( + fallbackUri, + mode: LaunchMode.externalApplication, + ); + } + } + } catch (e) { + logger.w('Failed to open ${wallet.name}: $e'); + } + } + Future _openGenericWallet(String walletName) async { + try { + final List commonSchemes = [ + '${walletName.replaceAll(' ', '').toLowerCase()}://', + '${walletName.replaceAll(' ', '').toLowerCase()}wallet://', + '${walletName.split(' ').first.toLowerCase()}://', + ]; + + for (final scheme in commonSchemes) { + try { + final Uri uri = Uri.parse('${scheme}wc'); + if (await canLaunchUrl(uri)) { + final launched = await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + if (launched) { + logger.d('Successfully opened wallet with scheme: $scheme'); + return; + } + } + } catch (e) { + continue; + } + } + } catch (e) { + logger.w('Failed to open wallet with generic approach: $e'); + } + } + Future signMessage(String message, {String? chainId}) async { + try { + if (!_isConnected || _web3App == null || _currentAddress == null) { + throw Exception('Wallet not connected'); + } + _updateStatus('Signing message...'); + final sessions = _web3App!.sessions.getAll(); + if (sessions.isEmpty) { + throw Exception('No active WalletConnect session'); + } + final targetChainId = chainId ?? _currentChainId ?? _defaultChainId; + final result = await _web3App!.request( + topic: sessions.first.topic, + chainId: 'eip155:$targetChainId', + request: SessionRequestParams( + method: 'personal_sign', + params: [ + '0x${utf8.encode(message).map((byte) => byte.toRadixString(16).padLeft(2, '0')).join()}', + _currentAddress!, + ], + ), + ); + final signature = result.toString(); + _updateStatus('Message signed'); - - + return signature; + } catch (e) { + _updateStatus('Message signing failed: ${e.toString()}'); + logger.e('Error signing message: $e'); + throw Exception('Failed to sign message: $e'); + } + } Future disconnectWallet() async { if (!_isConnected) return; From 79cc140296abb9145ac1fb6920372fe8722edba1 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 26 Jul 2025 00:25:56 +0530 Subject: [PATCH 09/18] refactor: file structuring and imports --- lib/models/wallet_chain_option.dart | 104 +++++++++++----------------- lib/providers/wallet_provider.dart | 3 +- 2 files changed, 41 insertions(+), 66 deletions(-) diff --git a/lib/models/wallet_chain_option.dart b/lib/models/wallet_chain_option.dart index d0effe3..1249404 100644 --- a/lib/models/wallet_chain_option.dart +++ b/lib/models/wallet_chain_option.dart @@ -1,5 +1,7 @@ -// models/wallet_option.dart import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +final String ALCHEMY_API_KEY = dotenv.env['ALCHEMY_API_KEY'] ?? ''; class WalletOption { final String name; @@ -17,68 +19,42 @@ class WalletOption { }); } - final List walletOptionsList = [ - WalletOption( - name: 'MetaMask', - deepLink: 'metamask://wc?uri=', - fallbackUrl: 'https://metamask.app.link/wc?uri=', - icon: Icons.account_balance_wallet, - color: Colors.orange, - ), - WalletOption( - name: 'Trust Wallet', - deepLink: 'trust://wc?uri=', - fallbackUrl: 'https://link.trustwallet.com/wc?uri=', - icon: Icons.security, - color: Colors.blue, - ), - WalletOption( - name: 'Rainbow', - deepLink: 'rainbow://wc?uri=', - fallbackUrl: 'https://rnbwapp.com/wc?uri=', - icon: Icons.colorize, - color: Colors.purple, - ), - WalletOption( - name: 'Coinbase Wallet', - deepLink: 'cbwallet://wc?uri=', - fallbackUrl: 'https://go.cb-w.com/wc?uri=', - icon: Icons.currency_bitcoin, - color: Colors.blue.shade700, - ), - ]; - - - final Map rpcUrls = { - '11155111': - 'https://eth-sepolia.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', - '1': 'https://eth-mainnet.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', - }; - - - final Map> chainInfoList = { - '1': { - 'name': 'Ethereum Mainnet', - 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', - 'nativeCurrency': { - 'name': 'Ether', - 'symbol': 'ETH', - 'decimals': 18, - }, - 'blockExplorerUrl': 'https://etherscan.io', +final List walletOptionsList = [ + WalletOption( + name: 'MetaMask', + deepLink: 'metamask://wc?uri=', + fallbackUrl: 'https://metamask.app.link/wc?uri=', + icon: Icons.account_balance_wallet, + color: Colors.orange, + ), +]; + +final Map rpcUrls = { + '11155111': + 'https://eth-sepolia.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', + '1': 'https://eth-mainnet.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', +}; + +final Map> chainInfoList = { + '1': { + 'name': 'Ethereum Mainnet', + 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_API_KEY', + 'nativeCurrency': { + 'name': 'Ether', + 'symbol': 'ETH', + 'decimals': 18, }, - '11155111': { - 'name': 'Sepolia Testnet', - 'rpcUrl': - 'https://eth-sepolia.g.alchemy.com/v2/ghiIjYuaumHfkffONpzBEItpKXWt9952', - 'nativeCurrency': { - 'name': 'Sepolia Ether', - 'symbol': 'SEP', - 'decimals': 18, - }, - 'blockExplorerUrl': 'https://sepolia.etherscan.io', + 'blockExplorerUrl': 'https://etherscan.io', + }, + '11155111': { + 'name': 'Sepolia Testnet', + 'rpcUrl': + 'https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY', + 'nativeCurrency': { + 'name': 'Sepolia Ether', + 'symbol': 'SEP', + 'decimals': 18, }, - }; - - - + 'blockExplorerUrl': 'https://sepolia.etherscan.io', + }, +}; diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 970a866..9ee8c12 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -11,7 +11,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'dart:convert'; -import 'package:web3dart/web3dart.dart'; import 'package:http/http.dart' as http; enum InitializationState { @@ -373,7 +372,7 @@ class WalletProvider extends ChangeNotifier { Future switchChain(String newChainId) async { logger.d('[switchChain] Requested chain id: $newChainId'); if (!_isConnected) { - print('[switchChain] Wallet not connected.'); + logger.e('[switchChain] Wallet not connected.'); throw Exception('Wallet not connected'); } From 4174c429157dd4d8e0b8482d26c15b5bc4cf9441 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 26 Jul 2025 11:34:31 +0530 Subject: [PATCH 10/18] add: geohash functionality --- lib/pages/counter_page.dart | 10 - lib/pages/mint_nft/mint_nft_coordinates.dart | 10 + lib/utils/services/counter_services.dart | 288 ------------------- lib/utils/services/ipfs_services.dart | 7 - lib/widgets/tree_NFT_view_widget.dart | 4 + pubspec.lock | 8 + pubspec.yaml | 2 + 7 files changed, 24 insertions(+), 305 deletions(-) delete mode 100644 lib/utils/services/counter_services.dart diff --git a/lib/pages/counter_page.dart b/lib/pages/counter_page.dart index 690a543..14063c4 100644 --- a/lib/pages/counter_page.dart +++ b/lib/pages/counter_page.dart @@ -111,8 +111,6 @@ class _CounterPageState extends State { _showSuccessDialog('Transaction Sent!', 'Transaction hash: ${txHash.substring(0, 10)}...\n\nThe counter will update once the transaction is confirmed.'); - - // Auto-refresh count after a delay to allow transaction confirmation Future.delayed(const Duration(seconds: 3), () { if (mounted) { _loadCount(); @@ -204,8 +202,6 @@ class _CounterPageState extends State { ), const SizedBox(height: 24), - - // Count Display Card Card( elevation: 4, color: Theme.of(context).primaryColor.withOpacity(0.1), @@ -287,8 +283,6 @@ class _CounterPageState extends State { ), const SizedBox(height: 24), - - // Action Buttons Row( children: [ Expanded( @@ -335,8 +329,6 @@ class _CounterPageState extends State { ), ], ), - - // Transaction Hash Display if (lastTransactionHash != null) ...[ const SizedBox(height: 16), Card( @@ -383,8 +375,6 @@ class _CounterPageState extends State { ], const Spacer(), - - // Status Information Consumer( builder: (context, walletProvider, child) { return Card( diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index 4214571..40d64e9 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -5,6 +5,7 @@ import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; +import 'package:dart_geohash/dart_geohash.dart'; class MintNftCoordinatesPage extends StatefulWidget { const MintNftCoordinatesPage({super.key}); @@ -16,10 +17,16 @@ class MintNftCoordinatesPage extends StatefulWidget { class _MintNftCoordinatesPageState extends State { final latitudeController = TextEditingController(); final longitudeController = TextEditingController(); + var geoHasher = GeoHasher(); void submitCoordinates() { final latitude = latitudeController.text; final longitude = longitudeController.text; + final geohash = geoHasher.encode( + double.parse(latitude), + double.parse(longitude), + precision: 12, + ); if (latitude.isEmpty || longitude.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -32,6 +39,9 @@ class _MintNftCoordinatesPageState extends State { .setLatitude(double.parse(latitude)); Provider.of(context, listen: false) .setLongitude(double.parse(longitude)); + Provider.of(context, listen: false) + .setGeoHash(geohash); + latitudeController.clear(); longitudeController.clear(); diff --git a/lib/utils/services/counter_services.dart b/lib/utils/services/counter_services.dart deleted file mode 100644 index 3a94ea2..0000000 --- a/lib/utils/services/counter_services.dart +++ /dev/null @@ -1,288 +0,0 @@ -// import 'package:web3dart/web3dart.dart'; -// import 'package:tree_planting_protocol/providers/wallet_provider.dart'; - -// class CounterService { -// // Sepolia testnet chain ID -// static const int SEPOLIA_CHAIN_ID = 11155111; - -// // ABI for a simple counter contract -// static const String counterABI = ''' -// [ -// { -// "inputs": [], -// "name": "count", -// "outputs": [ -// { -// "internalType": "uint256", -// "name": "", -// "type": "uint256" -// } -// ], -// "stateMutability": "view", -// "type": "function" -// }, -// { -// "inputs": [], -// "name": "increment", -// "outputs": [], -// "stateMutability": "nonpayable", -// "type": "function" -// }, -// { -// "inputs": [], -// "name": "getCount", -// "outputs": [ -// { -// "internalType": "uint256", -// "name": "", -// "type": "uint256" -// } -// ], -// "stateMutability": "view", -// "type": "function" -// } -// ] -// '''; - -// /// Get the current count from the smart contract -// static Future getCount( -// WalletProvider walletProvider, -// String contractAddress, -// ) async { -// try { -// // Ensure we have a web3 client -// if (walletProvider.ethClient == null) { -// throw Exception('Web3Client not initialized'); -// } - -// // Verify we're on the correct chain (Sepolia) -// await _verifyChainId(walletProvider); - -// // Create contract instance -// final contract = DeployedContract( -// ContractAbi.fromJson(counterABI, 'Counter'), -// EthereumAddress.fromHex(contractAddress), -// ); - -// // Get the count function -// final countFunction = contract.function('getCount') ?? -// contract.function('count'); - -// if (countFunction == null) { -// throw Exception('Count function not found in contract'); -// } - -// // Call the contract function -// final result = await walletProvider.ethClient!.call( -// contract: contract, -// function: countFunction, -// params: [], -// ); - -// // Return the count value -// if (result.isNotEmpty && result[0] is BigInt) { -// return result[0] as BigInt; -// } else { -// throw Exception('Invalid response from contract'); -// } -// } catch (e) { -// throw Exception('Failed to get count: ${e.toString()}'); -// } -// } - -// /// Increment the counter by calling the smart contract -// static Future increment( -// WalletProvider walletProvider, -// String contractAddress, -// ) async { -// try { -// // Ensure wallet is connected -// if (!walletProvider.isConnected || walletProvider.currentAddress!.isEmpty) { -// throw Exception('Wallet not connected'); -// } - -// // Ensure we're on Sepolia -// await _verifyChainId(walletProvider); - -// // Ensure we have a web3 client -// if (walletProvider.ethClient == null) { -// throw Exception('Web3Client not initialized'); -// } - -// // Create contract instance -// final contract = DeployedContract( -// ContractAbi.fromJson(counterABI, 'Counter'), -// EthereumAddress.fromHex(contractAddress), -// ); - -// // Get the increment function -// final incrementFunction = contract.function('increment'); -// if (incrementFunction == null) { -// throw Exception('Increment function not found in contract'); -// } - -// // Get current gas price -// final gasPrice = await walletProvider.ethClient!.getGasPrice(); - -// // Estimate gas for the transaction -// BigInt gasLimit; -// try { -// gasLimit = await walletProvider.ethClient!.estimateGas( -// sender: EthereumAddress.fromHex(walletProvider.currentAddress!), -// to: EthereumAddress.fromHex(contractAddress), -// data: incrementFunction.encodeCall([]), -// ); -// // Add 20% buffer to gas limit -// gasLimit = BigInt.from((gasLimit.toDouble() * 1.2).round()); -// } catch (e) { -// // Fallback gas limit if estimation fails -// gasLimit = BigInt.from(100000); -// } - -// // Create the transaction -// final transaction = Transaction.callContract( -// contract: contract, -// function: incrementFunction, -// parameters: [], -// from: EthereumAddress.fromHex(walletProvider.currentAddress!), -// gasPrice: gasPrice, -// maxGas: gasLimit.toInt(), -// ); - -// // Send the transaction -// final txHash = await walletProvider.sendTransaction(transaction -// , contractAddress: contractAddress, -// functionName: 'increment', -// parameters: [], -// ); - -// return txHash; -// } catch (e) { -// throw Exception('Failed to increment: ${e.toString()}'); -// } -// } - -// /// Get transaction receipt and status -// static Future getTransactionReceipt( -// WalletProvider walletProvider, -// String txHash, -// ) async { -// try { -// if (walletProvider.ethClient == null) { -// throw Exception('Web3Client not initialized'); -// } - -// return await walletProvider.ethClient!.getTransactionReceipt(txHash); -// } catch (e) { -// throw Exception('Failed to get transaction receipt: ${e.toString()}'); -// } -// } - -// /// Wait for transaction confirmation -// static Future waitForTransaction( -// WalletProvider walletProvider, -// String txHash, { -// Duration timeout = const Duration(minutes: 5), -// Duration pollInterval = const Duration(seconds: 2), -// }) async { -// final startTime = DateTime.now(); - -// while (DateTime.now().difference(startTime) < timeout) { -// try { -// final receipt = await getTransactionReceipt(walletProvider, txHash); -// if (receipt != null) { -// return receipt; -// } -// } catch (e) { -// // Continue polling on error -// } - -// await Future.delayed(pollInterval); -// } - -// throw Exception('Transaction confirmation timeout'); -// } - -// /// Get the Sepolia Etherscan URL for a transaction -// static String getEtherscanUrl(String txHash) { -// return 'https://sepolia.etherscan.io/tx/$txHash'; -// } - -// /// Validate contract address format -// static bool isValidContractAddress(String address) { -// try { -// EthereumAddress.fromHex(address); -// return true; -// } catch (e) { -// return false; -// } -// } - -// /// Format BigInt count for display -// static String formatCount(BigInt count) { -// return count.toString(); -// } - -// /// Convert Wei to Ether for gas calculations -// static String weiToEther(BigInt wei) { -// return EtherAmount.inWei(wei).getValueInUnit(EtherUnit.ether).toString(); -// } - -// /// Verify that we're connected to the Sepolia testnet -// static Future _verifyChainId(WalletProvider walletProvider) async { -// try { -// final chainId = await walletProvider.ethClient!.getChainId(); -// if (chainId != SEPOLIA_CHAIN_ID) { -// throw Exception( -// 'Wrong network! Please switch to Sepolia testnet (Chain ID: $SEPOLIA_CHAIN_ID). ' -// 'Current Chain ID: $chainId' -// ); -// } -// } catch (e) { -// if (e.toString().contains('Wrong network')) { -// rethrow; -// } -// throw Exception('Failed to verify chain ID: ${e.toString()}'); -// } -// } - -// /// Get current chain ID -// static Future getCurrentChainId(WalletProvider walletProvider) async { -// try { -// if (walletProvider.ethClient == null) { -// throw Exception('Web3Client not initialized'); -// } -// return await walletProvider.ethClient!.getChainId(); -// } catch (e) { -// throw Exception('Failed to get chain ID: ${e.toString()}'); -// } -// } - -// /// Check if currently connected to Sepolia -// static Future isConnectedToSepolia(WalletProvider walletProvider) async { -// try { -// final chainId = await getCurrentChainId(walletProvider); -// return chainId == SEPOLIA_CHAIN_ID; -// } catch (e) { -// return false; -// } -// } - -// /// Get chain name from chain ID -// static String getChainName(int chainId) { -// switch (chainId) { -// case 1: -// return 'Ethereum Mainnet'; -// case 5: -// return 'Goerli Testnet'; -// case 11155111: -// return 'Sepolia Testnet'; -// case 137: -// return 'Polygon Mainnet'; -// case 80001: -// return 'Polygon Mumbai'; -// default: -// return 'Unknown Network (ID: $chainId)'; -// } -// } -// } \ No newline at end of file diff --git a/lib/utils/services/ipfs_services.dart b/lib/utils/services/ipfs_services.dart index 89b3966..bdad8dd 100644 --- a/lib/utils/services/ipfs_services.dart +++ b/lib/utils/services/ipfs_services.dart @@ -28,10 +28,3 @@ Future uploadToIPFS(File imageFile, Function(bool) setUploadingState) a return null; } } - -// Usage in your main file: -// String? result = await uploadToIPFS(imageFile, (isUploading) { -// setState(() { -// _isUploading = isUploading; -// }); -// }); \ No newline at end of file diff --git a/lib/widgets/tree_NFT_view_widget.dart b/lib/widgets/tree_NFT_view_widget.dart index ea8c96b..e4cba56 100644 --- a/lib/widgets/tree_NFT_view_widget.dart +++ b/lib/widgets/tree_NFT_view_widget.dart @@ -38,6 +38,10 @@ class _NewNFTWidgetState extends State { 'Longitude: ${Provider.of(ctx, listen: true).getLongitude()}', style: const TextStyle(fontSize: 20), ), + Text( + 'GeoHash: ${(Provider.of(ctx, listen: true).getGeoHash())}', + style: const TextStyle(fontSize: 20), + ), Text( 'Species: ${Provider.of(ctx, listen: true).getSpecies()}', style: const TextStyle(fontSize: 20), diff --git a/pubspec.lock b/pubspec.lock index ea51d66..61ff11b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.5" + dart_geohash: + dependency: "direct main" + description: + name: dart_geohash + sha256: c5594c0063ca67f37778b2c7d025d22e2ecd38741c0acd5b0f7596374e3309f2 + url: "https://pub.dev" + source: hosted + version: "2.1.0" dbus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e278fe7..08fbf8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: flutter_dotenv: ^5.1.0 image_picker: ^1.0.4 logger: ^2.0.2+1 + dart_geohash: ^2.1.0 + dev_dependencies: flutter_test: From 05f096d60bd8b7a83b82be95abb88f1e5c99b476 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 26 Jul 2025 12:06:50 +0530 Subject: [PATCH 11/18] add: switch chain functionality in the navbar --- lib/components/universal_navbar.dart | 83 +++++++++--------- lib/pages/settings_page.dart | 42 +++++++++ lib/pages/switch_chain_page.dart | 99 +++------------------- lib/providers/wallet_provider.dart | 2 + lib/utils/constants/tree_images.dart | 15 ++++ lib/utils/services/switch_chain_utils.dart | 85 +++++++++++++++++++ 6 files changed, 198 insertions(+), 128 deletions(-) create mode 100644 lib/pages/settings_page.dart create mode 100644 lib/utils/constants/tree_images.dart create mode 100644 lib/utils/services/switch_chain_utils.dart diff --git a/lib/components/universal_navbar.dart b/lib/components/universal_navbar.dart index 782c94f..286ee32 100644 --- a/lib/components/universal_navbar.dart +++ b/lib/components/universal_navbar.dart @@ -5,6 +5,8 @@ import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/providers/theme_provider.dart'; import 'package:tree_planting_protocol/components/wallet_connect_dialog.dart'; import 'package:tree_planting_protocol/utils/services/wallet_provider_utils.dart'; +import 'package:tree_planting_protocol/utils/constants/tree_images.dart'; +import 'package:tree_planting_protocol/utils/services/switch_chain_utils.dart'; class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { final String? title; @@ -14,24 +16,6 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { @override Size get preferredSize => const Size.fromHeight(120.0); - - // Updated to use PNG file names instead of icons - final List treeImages = const [ - 'tree-1.png', - 'tree-2.png', - 'tree-3.png', - 'tree-4.png', - 'tree-5.png', - 'tree-6.png', - 'tree-7.png', - 'tree-8.png', - 'tree-9.png', - 'tree-10.png', - 'tree-11.png', - 'tree-12.png', - 'tree-13.png', - ]; - @override Widget build(BuildContext context) { final walletProvider = Provider.of(context); @@ -39,7 +23,9 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { return Container( height: 140, decoration: BoxDecoration( - color: themeProvider.isDarkMode ? const Color.fromARGB(255, 1, 135, 12) : const Color.fromARGB(255, 28, 211, 129), + color: themeProvider.isDarkMode + ? const Color.fromARGB(255, 1, 135, 12) + : const Color.fromARGB(255, 28, 211, 129), ), child: Stack( children: [ @@ -72,7 +58,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { width: 1, ), ), - child: Image.asset( + child: Image.asset( 'assets/tree-navbar-images/logo.png', // Fixed path to match your folder structure width: 28, height: 28, @@ -82,9 +68,9 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { Icons.eco, color: Colors.green[600], size: 28, - ); - }, - ), + ); + }, + ), ), const SizedBox(width: 8), if (title != null) @@ -129,8 +115,8 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { child: IconButton( padding: EdgeInsets.zero, icon: Icon( - themeProvider.isDarkMode - ? Icons.light_mode + themeProvider.isDarkMode + ? Icons.light_mode : Icons.dark_mode, color: Colors.white, size: 18, @@ -138,16 +124,15 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { onPressed: () { themeProvider.toggleTheme(); }, - tooltip: themeProvider.isDarkMode - ? 'Switch to Light Mode' + tooltip: themeProvider.isDarkMode + ? 'Switch to Light Mode' : 'Switch to Dark Mode', ), ), - const SizedBox(width: 6), if (actions != null) ...actions!, - - if (walletProvider.isConnected && walletProvider.currentAddress != null) + if (walletProvider.isConnected && + walletProvider.currentAddress != null) _buildWalletMenu(context, walletProvider) else _buildConnectButton(context, walletProvider), @@ -166,7 +151,8 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { Widget _buildPlantIllustrations() { return Container( decoration: BoxDecoration( - color: const Color.fromARGB(255, 251, 251, 99).withOpacity(0.9), // Beige background + color: const Color.fromARGB(255, 251, 251, 99) + .withOpacity(0.9), // Beige background borderRadius: const BorderRadius.only( topLeft: Radius.circular(40), topRight: Radius.circular(40), @@ -185,13 +171,15 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { builder: (context, constraints) { final availableWidth = constraints.maxWidth; final plantWidth = 35.0; - final plantSpacing = 0.0; + final plantSpacing = 0.0; final totalPlantWidth = plantWidth + plantSpacing; - final visiblePlantCount = (availableWidth / totalPlantWidth).floor(); - + final visiblePlantCount = + (availableWidth / totalPlantWidth).floor(); + if (visiblePlantCount >= treeImages.length) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 0.0, vertical: 2.5), + padding: + const EdgeInsets.symmetric(horizontal: 0.0, vertical: 2.5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: treeImages.map((imagePath) { @@ -216,7 +204,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -329,6 +317,10 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { ); } } + + else if (value == 'Switch Chain') { + showChainSelector(context, walletProvider); + } }, itemBuilder: (BuildContext context) => [ const PopupMenuItem( @@ -339,6 +331,17 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { contentPadding: EdgeInsets.symmetric(horizontal: 8), ), ), + const PopupMenuItem( + value: 'Switch Chain', + child: ListTile( + leading: Icon(Icons.logout, color: Colors.red, size: 20), + title: Text( + 'Switch Chain', + style: TextStyle(color: Colors.green), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 8), + ), + ), const PopupMenuItem( value: 'disconnect', child: ListTile( @@ -355,7 +358,8 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { ); } - Widget _buildConnectButton(BuildContext context, WalletProvider walletProvider) { + Widget _buildConnectButton( + BuildContext context, WalletProvider walletProvider) { return Container( constraints: const BoxConstraints(maxWidth: 80), // Limit max width decoration: BoxDecoration( @@ -387,7 +391,8 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { } }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -415,4 +420,4 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart new file mode 100644 index 0000000..f4199ca --- /dev/null +++ b/lib/pages/settings_page.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/utils/services/switch_chain_utils.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + @override + Widget build(BuildContext context) { + return BaseScaffold( + title: "Settings", + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Consumer( + builder: (ctx, walletProvider, __) => Column( + children: [ + Text( + 'User Address: ${walletProvider.userAddress}', + ), + Text( + 'Current Chain: ${walletProvider.currentChainName} (${walletProvider.currentChainId})', + style: const TextStyle(fontSize: 20), + ), + ElevatedButton( + onPressed: () => showChainSelector(context, walletProvider), + child: const Text('Switch Chain'), + ), + ], + ), + ) + ], + )); + } +} diff --git a/lib/pages/switch_chain_page.dart b/lib/pages/switch_chain_page.dart index a2f3ea7..cb44f52 100644 --- a/lib/pages/switch_chain_page.dart +++ b/lib/pages/switch_chain_page.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; + import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/utils/services/switch_chain_utils.dart'; + class SwitchChainPage extends StatelessWidget { const SwitchChainPage({Key? key}) : super(key: key); @@ -37,9 +40,9 @@ class SwitchChainPage extends StatelessWidget { ), const SizedBox(height: 8), Text( - walletProvider.isConnected - ? '${walletProvider.currentChainName} (${walletProvider.currentChainId})' - : 'Not Connected', + walletProvider.isConnected + ? '${walletProvider.currentChainName} (${walletProvider.currentChainId})' + : 'Not Connected', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, @@ -69,9 +72,9 @@ class SwitchChainPage extends StatelessWidget { width: double.infinity, height: 50, child: ElevatedButton( - onPressed: walletProvider.isConnected - ? () => _showChainSelector(context, walletProvider) - : null, + onPressed: walletProvider.isConnected + ? () => showChainSelector(context, walletProvider) + : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, @@ -90,7 +93,6 @@ class SwitchChainPage extends StatelessWidget { ), ), ), - if (!walletProvider.isConnected) ...[ const SizedBox(height: 16), Text( @@ -109,85 +111,4 @@ class SwitchChainPage extends StatelessWidget { ), ); } - - void _showChainSelector(BuildContext context, WalletProvider walletProvider) { - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Select Chain', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - ...walletProvider.getSupportedChains().map((chain) { - final isCurrentChain = chain['isCurrentChain'] as bool; - return Container( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - title: Text( - chain['name'] as String, - style: TextStyle( - fontWeight: isCurrentChain ? FontWeight.bold : FontWeight.normal, - ), - ), - subtitle: Text('Chain ID: ${chain['chainId']}'), - trailing: isCurrentChain - ? const Icon(Icons.check_circle, color: Colors.green) - : const Icon(Icons.arrow_forward_ios), - tileColor: isCurrentChain ? Colors.green[50] : null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: isCurrentChain ? Colors.green : Colors.grey[300]!, - ), - ), - onTap: isCurrentChain ? null : () async { - Navigator.pop(context); - await _switchToChain(context, walletProvider, chain['chainId'] as String); - }, - ), - ); - }).toList(), - const SizedBox(height: 16), - ], - ), - ); - }, - ); - } - - Future _switchToChain(BuildContext context, WalletProvider walletProvider, String chainId) async { - try { - final success = await walletProvider.switchChain(chainId); - if (success && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Chain switched successfully!'), - backgroundColor: Colors.green, - ), - ); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to switch chain: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - } -} \ No newline at end of file +} diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 9ee8c12..917ce15 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -247,6 +247,8 @@ class WalletProvider extends ChangeNotifier { final Map> _chainInfo = chainInfoList; Map> get chainInfo => chainInfoList; + get userAddress => null; + List> getSupportedChains() { return _chainInfo.entries.map((entry) { return { diff --git a/lib/utils/constants/tree_images.dart b/lib/utils/constants/tree_images.dart new file mode 100644 index 0000000..cc57c8f --- /dev/null +++ b/lib/utils/constants/tree_images.dart @@ -0,0 +1,15 @@ +final List treeImages = const [ + 'tree-1.png', + 'tree-2.png', + 'tree-3.png', + 'tree-4.png', + 'tree-5.png', + 'tree-6.png', + 'tree-7.png', + 'tree-8.png', + 'tree-9.png', + 'tree-10.png', + 'tree-11.png', + 'tree-12.png', + 'tree-13.png', + ]; diff --git a/lib/utils/services/switch_chain_utils.dart b/lib/utils/services/switch_chain_utils.dart new file mode 100644 index 0000000..128faf0 --- /dev/null +++ b/lib/utils/services/switch_chain_utils.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; + + void showChainSelector(BuildContext context, WalletProvider walletProvider) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Select Chain', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ...walletProvider.getSupportedChains().map((chain) { + final isCurrentChain = chain['isCurrentChain'] as bool; + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + title: Text( + chain['name'] as String, + style: TextStyle( + fontWeight: isCurrentChain ? FontWeight.bold : FontWeight.normal, + ), + ), + subtitle: Text('Chain ID: ${chain['chainId']}'), + trailing: isCurrentChain + ? const Icon(Icons.check_circle, color: Colors.green) + : const Icon(Icons.arrow_forward_ios), + tileColor: isCurrentChain ? Colors.green[50] : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: isCurrentChain ? Colors.green : Colors.grey[300]!, + ), + ), + onTap: isCurrentChain ? null : () async { + Navigator.pop(context); + await switchToChain(context, walletProvider, chain['chainId'] as String); + }, + ), + ); + }).toList(), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } + + Future switchToChain(BuildContext context, WalletProvider walletProvider, String chainId) async { + try { + final success = await walletProvider.switchChain(chainId); + if (success && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Chain switched successfully!'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to switch chain: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } From cd5958b2afc29d0e9a92fdc0033a082e9b502256 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 26 Jul 2025 12:30:03 +0530 Subject: [PATCH 12/18] fix: overflow error --- lib/components/universal_navbar.dart | 19 ++++++++------ lib/main.dart | 10 +++++++- lib/pages/settings_page.dart | 2 ++ lib/pages/trees_page.dart | 25 +++++++++++++------ lib/utils/constants/bottom_nav_constants.dart | 13 +++++----- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/lib/components/universal_navbar.dart b/lib/components/universal_navbar.dart index 286ee32..72062d8 100644 --- a/lib/components/universal_navbar.dart +++ b/lib/components/universal_navbar.dart @@ -266,15 +266,18 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { color: Colors.green[700], ), const SizedBox(width: 4), - Flexible( - child: Text( - formatAddress(walletProvider.currentAddress!), - style: TextStyle( - color: Colors.green[700], - fontWeight: FontWeight.w600, - fontSize: 10, + Container( + width: 10, + child: Flexible( + child: Text( + formatAddress(walletProvider.currentAddress!), + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.w600, + fontSize: 10, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), ), Icon( diff --git a/lib/main.dart b/lib/main.dart index 38b51fa..66321c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_images.dart'; +import 'package:tree_planting_protocol/pages/settings_page.dart'; import 'package:tree_planting_protocol/pages/switch_chain_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; @@ -48,6 +49,13 @@ class MyApp extends StatelessWidget { return const HomePage(); }, ), + GoRoute( + path: '/settings', + name: 'settings_page', + builder: (BuildContext context, GoRouterState state) { + return const SettingsPage(); + }, + ), GoRoute( path: '/counter', name: 'counter_page', @@ -79,7 +87,7 @@ class MyApp extends StatelessWidget { path: RouteConstants.allTreesPath, name: RouteConstants.allTrees, builder: (BuildContext context, GoRouterState state) { - return const SwitchChainPage(); + return const AllTreesPage(); }, routes: [ GoRoute( diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index f4199ca..3029e4d 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -21,6 +21,8 @@ class _SettingsPageState extends State { children: [ Consumer( builder: (ctx, walletProvider, __) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( 'User Address: ${walletProvider.userAddress}', diff --git a/lib/pages/trees_page.dart b/lib/pages/trees_page.dart index 337deb3..f0133ba 100644 --- a/lib/pages/trees_page.dart +++ b/lib/pages/trees_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/utils/constants/navbar_constants.dart'; @@ -9,15 +10,23 @@ class AllTreesPage extends StatelessWidget { Widget build(BuildContext context) { return BaseScaffold( title: appName, - body: Center( - child: Text( - 'This page will display all the trees on chain', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface, + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.push('/mint-nft'), + child: const Text( + 'Mint NFT', + textAlign: TextAlign.center, + ), + + ), + Text( + 'This is the All Trees Page', + style: Theme.of(context).textTheme.headlineLarge, ), - textAlign: TextAlign.center, - ), - ), + ], + ) ); } } \ No newline at end of file diff --git a/lib/utils/constants/bottom_nav_constants.dart b/lib/utils/constants/bottom_nav_constants.dart index bb7d587..f1915b9 100644 --- a/lib/utils/constants/bottom_nav_constants.dart +++ b/lib/utils/constants/bottom_nav_constants.dart @@ -29,17 +29,18 @@ class BottomNavConstants { activeIcon: Icons.forest, route: RouteConstants.allTreesPath, ), - BottomNavItem( - label: 'Mint NFT', - icon: Icons.nature_people_outlined, - activeIcon: Icons.nature_people, - route: RouteConstants.mintNftPath, - ), BottomNavItem( label: 'Counter', icon: Icons.nature_people_outlined, activeIcon: Icons.nature_people, route: '/counter', ), + + BottomNavItem( + label: 'Settings', + icon: Icons.settings_outlined, + activeIcon: Icons.settings, + route: '/settings', + ), ]; } \ No newline at end of file From 2e09696347c8fd9d7f6f8afb11613e5eb094fc2b Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 26 Jul 2025 12:34:46 +0530 Subject: [PATCH 13/18] fix: widget spacing --- lib/main.dart | 1 - lib/pages/switch_chain_page.dart | 114 ------------------------------- lib/pages/trees_page.dart | 34 +++++---- 3 files changed, 19 insertions(+), 130 deletions(-) delete mode 100644 lib/pages/switch_chain_page.dart diff --git a/lib/main.dart b/lib/main.dart index 66321c0..328ce75 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_images.dart'; import 'package:tree_planting_protocol/pages/settings_page.dart'; -import 'package:tree_planting_protocol/pages/switch_chain_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; import 'package:tree_planting_protocol/pages/counter_page.dart'; diff --git a/lib/pages/switch_chain_page.dart b/lib/pages/switch_chain_page.dart deleted file mode 100644 index cb44f52..0000000 --- a/lib/pages/switch_chain_page.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:tree_planting_protocol/providers/wallet_provider.dart'; -import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; - -import 'package:tree_planting_protocol/utils/services/switch_chain_utils.dart'; - -class SwitchChainPage extends StatelessWidget { - const SwitchChainPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BaseScaffold( - body: Consumer( - builder: (context, walletProvider, child) { - return Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[300]!), - ), - child: Column( - children: [ - const Text( - 'Current Chain', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - const SizedBox(height: 8), - Text( - walletProvider.isConnected - ? '${walletProvider.currentChainName} (${walletProvider.currentChainId})' - : 'Not Connected', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: Colors.blue[50], - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue[200]!), - ), - child: Text( - walletProvider.statusMessage, - style: TextStyle( - color: Colors.blue[800], - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.center, - ), - ), - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: walletProvider.isConnected - ? () => showChainSelector(context, walletProvider) - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - disabledBackgroundColor: Colors.grey[300], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, - ), - child: const Text( - 'Switch Chain', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - if (!walletProvider.isConnected) ...[ - const SizedBox(height: 16), - Text( - 'Please connect your wallet first', - style: TextStyle( - color: Colors.grey[600], - fontSize: 14, - ), - ), - ], - ], - ), - ), - ); - }, - ), - ); - } -} diff --git a/lib/pages/trees_page.dart b/lib/pages/trees_page.dart index f0133ba..9411f26 100644 --- a/lib/pages/trees_page.dart +++ b/lib/pages/trees_page.dart @@ -10,23 +10,27 @@ class AllTreesPage extends StatelessWidget { Widget build(BuildContext context) { return BaseScaffold( title: appName, - body: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => context.push('/mint-nft'), - child: const Text( - 'Mint NFT', + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'This page will display all the recent and nearby trees.', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), textAlign: TextAlign.center, ), - - ), - Text( - 'This is the All Trees Page', - style: Theme.of(context).textTheme.headlineLarge, - ), - ], - ) + const SizedBox(height: 32), + ElevatedButton( + onPressed: () { + context.push('/mint-nft'); + }, + child: const Text('Mint a new NFT'), + ), + ], + ), + ), ); } } \ No newline at end of file From 04fd5567f8c17a08fa2d9b9ee11dd5d40abf58d1 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sun, 27 Jul 2025 19:36:46 +0530 Subject: [PATCH 14/18] add: flutter map --- android/app/src/main/AndroidManifest.xml | 15 + lib/pages/counter_page.dart | 5 +- lib/pages/mint_nft/mint_nft_coordinates.dart | 79 ++-- lib/pages/mint_nft/mint_nft_details.dart | 81 ++-- lib/pages/mint_nft/mint_nft_images.dart | 367 ++++++++++--------- lib/widgets/flutter_map_widget.dart | 148 ++++++++ lib/widgets/tree_NFT_view_widget.dart | 89 +++-- pubspec.lock | 80 ++++ pubspec.yaml | 2 + 9 files changed, 568 insertions(+), 298 deletions(-) create mode 100644 lib/widgets/flutter_map_widget.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7890679..d5699d4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,18 @@ + + + + + + + + + + + + + + @@ -8,6 +22,7 @@ { - static const String contractAddress = '0xa122109493B90e322824c3444ed8D6236CAbAB7C'; - static const String chainId = '11155111'; // Sepolia testnet + static final String contractAddress = dotenv.env['CONTRACT_ADDRESS'] ?? '0xa122109493B90e322824c3444ed8D6236CAbAB7C'; + static const String chainId = '11155111'; static const List> contractAbi = [ { diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index 40d64e9..c8373e5 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/flutter_map_widget.dart'; import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; import 'package:dart_geohash/dart_geohash.dart'; @@ -55,44 +56,52 @@ class _MintNftCoordinatesPageState extends State { Widget build(BuildContext context) { return BaseScaffold( title: "Mint NFT Coordinates", - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const NewNFTWidget(), - const SizedBox(height: 20), - const Text( - "Enter your coordinates", - style: TextStyle(fontSize: 30), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - TextField( - controller: latitudeController, - decoration: const InputDecoration( - labelText: "Latitude", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const NewNFTWidget(), + const SizedBox(height: 20), + const SizedBox( + height: 300, + width: 350, + child: CoordinatesMap() ), - ), - const SizedBox(height: 10), - TextField( - controller: longitudeController, - decoration: const InputDecoration( - labelText: "Longitude", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), + const Text( + "Enter your coordinates", + style: TextStyle(fontSize: 30), + textAlign: TextAlign.center, ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: submitCoordinates, - child: const Text( - "Next", - style: TextStyle(fontSize: 20, color: Colors.white), + const SizedBox(height: 20), + TextField( + controller: latitudeController, + decoration: const InputDecoration( + labelText: "Latitude", + border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), + ), ), - ) - ], + const SizedBox(height: 10), + TextField( + controller: longitudeController, + decoration: const InputDecoration( + labelText: "Longitude", + border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: submitCoordinates, + child: const Text( + "Next", + style: TextStyle(fontSize: 20, color: Colors.white), + ), + ), + ], + ), ), ), ); diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index c419913..65d0486 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -46,46 +46,49 @@ class _MintNftCoordinatesPageState extends State { return BaseScaffold( title: "NFT Details", body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const NewNFTWidget(), - const SizedBox(height: 20), - const Text( - "Enter NFT Details", - style: TextStyle(fontSize: 30), - textAlign: TextAlign.center, + child: SingleChildScrollView( + child: + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const NewNFTWidget(), + const SizedBox(height: 20), + const Text( + "Enter NFT Details", + style: TextStyle(fontSize: 30), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + TextField( + minLines: 4, + maxLines: 8, + controller: descriptionController, + decoration: const InputDecoration( + labelText: "Description", + border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300, maxHeight: 200), + alignLabelWithHint: true, + ), + ), + const SizedBox(height: 10), + TextField( + controller: speciesController, + decoration: const InputDecoration( + labelText: "Species", + border: OutlineInputBorder(), + constraints: BoxConstraints(maxWidth: 300), + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: submitDetails, + child: const Text( + "Next", + style: TextStyle(fontSize: 20, color: Colors.white), + ), + ) + ], ), - const SizedBox(height: 20), - TextField( - minLines: 4, - maxLines: 8, - controller: descriptionController, - decoration: const InputDecoration( - labelText: "Description", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300, maxHeight: 200), - alignLabelWithHint: true, - ), - ), - const SizedBox(height: 10), - TextField( - controller: speciesController, - decoration: const InputDecoration( - labelText: "Species", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), - ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: submitDetails, - child: const Text( - "Next", - style: TextStyle(fontSize: 20, color: Colors.white), - ), - ) - ], ), ), ); diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart index eaf94da..4f003b5 100644 --- a/lib/pages/mint_nft/mint_nft_images.dart +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -159,199 +159,127 @@ class _MultipleImageUploadPageState extends State { @override Widget build(BuildContext context) { return BaseScaffold( - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const NewNFTWidget(), - const SizedBox(height: 20), - Row( + body: SingleChildScrollView( + child: + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: _isUploading ? null : _pickImages, - icon: const Icon(Icons.photo_library), - label: const Text('Select Images'), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton.icon( - onPressed: (_selectedImages.isEmpty || _isUploading) - ? null - : _uploadAllImages, - icon: const Icon(Icons.cloud_upload), - label: const Text('Upload All'), - ), - ), - ], - ), - - const SizedBox(height: 16), - if (_isUploading) - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 8), - Text( - _uploadingIndex >= 0 - ? 'Uploading image ${_uploadingIndex + 1} of ${_selectedImages.length}...' - : 'Uploading...', - style: Theme.of(context).textTheme.bodyMedium, + const NewNFTWidget(), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isUploading ? null : _pickImages, + icon: const Icon(Icons.photo_library), + label: const Text('Select Images'), ), - ], - ), - ), - ), - - const SizedBox(height: 16), - if (_selectedImages.isNotEmpty) ...[ - Text( - 'Selected Images (${_selectedImages.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _selectedImages.length, - itemBuilder: (context, index) { - return Container( - width: 120, - margin: const EdgeInsets.only(right: 8), - child: Card( - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file( - _selectedImages[index], - width: 120, - height: 80, - fit: BoxFit.cover, - ), - ), - - Positioned( - top: 4, - right: 4, - child: GestureDetector( - onTap: () => _removeImage(index), - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.close, - color: Colors.white, - size: 16, - ), - ), - ), - ), - Positioned( - bottom: 4, - left: 4, - right: 4, - child: SizedBox( - height: 28, - child: ElevatedButton( - onPressed: (_isUploading && _uploadingIndex == index) - ? null - : () => _uploadSingleImage(index), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - child: (_isUploading && _uploadingIndex == index) - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.upload, size: 16), - ), - ), - ), - ], - ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton.icon( + onPressed: (_selectedImages.isEmpty || _isUploading) + ? null + : _uploadAllImages, + icon: const Icon(Icons.cloud_upload), + label: const Text('Upload All'), ), - ); - }, + ), + ], ), - ), - const SizedBox(height: 16), - ], - if (_uploadedHashes.isNotEmpty) ...[ - Text( - 'Uploaded Images (${_uploadedHashes.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - ], - Expanded( - child: _uploadedHashes.isEmpty - ? Center( + + const SizedBox(height: 16), + if (_isUploading) + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.cloud_off, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), + const CircularProgressIndicator(), + const SizedBox(height: 8), Text( - 'No images uploaded yet', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), + _uploadingIndex >= 0 + ? 'Uploading image ${_uploadingIndex + 1} of ${_selectedImages.length}...' + : 'Uploading...', + style: Theme.of(context).textTheme.bodyMedium, ), ], ), - ) - : ListView.builder( - itemCount: _uploadedHashes.length, + ), + ), + + const SizedBox(height: 16), + if (_selectedImages.isNotEmpty) ...[ + Text( + 'Selected Images (${_selectedImages.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _selectedImages.length, itemBuilder: (context, index) { - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - child: Text('${index + 1}'), - ), - title: Text( - 'Image ${index + 1}', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - _uploadedHashes[index], - style: const TextStyle(fontSize: 12), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, + return Container( + width: 120, + margin: const EdgeInsets.only(right: 8), + child: Card( + child: Stack( children: [ - IconButton( - icon: const Icon(Icons.open_in_new), - onPressed: () { - // You can implement opening the IPFS link here - _showSnackBar('IPFS Hash: ${_uploadedHashes[index]}'); - }, - tooltip: 'View on IPFS', + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + _selectedImages[index], + width: 120, + height: 80, + fit: BoxFit.cover, + ), + ), + + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => _removeImage(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), ), - IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () => _removeUploadedHash(index), - tooltip: 'Remove', + Positioned( + bottom: 4, + left: 4, + right: 4, + child: SizedBox( + height: 28, + child: ElevatedButton( + onPressed: (_isUploading && _uploadingIndex == index) + ? null + : () => _uploadSingleImage(index), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + ), + child: (_isUploading && _uploadingIndex == index) + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.upload, size: 16), + ), + ), ), ], ), @@ -359,9 +287,84 @@ class _MultipleImageUploadPageState extends State { ); }, ), + ), + const SizedBox(height: 16), + ], + if (_uploadedHashes.isNotEmpty) ...[ + Text( + 'Uploaded Images (${_uploadedHashes.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + ], + Expanded( + child: _uploadedHashes.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_off, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No images uploaded yet', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + ], + ), + ) + : ListView.builder( + itemCount: _uploadedHashes.length, + itemBuilder: (context, index) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + child: Text('${index + 1}'), + ), + title: Text( + 'Image ${index + 1}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + _uploadedHashes[index], + style: const TextStyle(fontSize: 12), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.open_in_new), + onPressed: () { + // You can implement opening the IPFS link here + _showSnackBar('IPFS Hash: ${_uploadedHashes[index]}'); + }, + tooltip: 'View on IPFS', + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _removeUploadedHash(index), + tooltip: 'Remove', + ), + ], + ), + ), + ); + }, + ), + ), + ], ), - ], - ), + ), ), ); } diff --git a/lib/widgets/flutter_map_widget.dart b/lib/widgets/flutter_map_widget.dart new file mode 100644 index 0000000..f13c73f --- /dev/null +++ b/lib/widgets/flutter_map_widget.dart @@ -0,0 +1,148 @@ +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CoordinatesMap extends StatefulWidget { + const CoordinatesMap({super.key}); + + @override + State createState() => _CoordinatesMapState(); +} + +class _CoordinatesMapState extends State { + late MapController _mapController; + bool _mapLoaded = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _mapController = MapController(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(28.7041, 77.1025), // Delhi coordinates + initialZoom: 10.0, + minZoom: 3.0, + maxZoom: 18.0, + onMapReady: () { + setState(() { + _mapLoaded = true; + }); + }, + onTap: (tapPosition, point) { + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'tree_planting_protocol', + retinaMode: true, + errorTileCallback: (tile, error, stackTrace) { + setState(() { + _errorMessage = 'Tile loading error: $error'; + }); + }, + tileBuilder: (context, tileWidget, tile) { + return tileWidget; + }, + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(28.7041, 77.1025), + width: 80, + height: 80, + child: Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + RichAttributionWidget( + attributions: [ + TextSourceAttribution( + 'OpenStreetMap contributors', + onTap: () => launchUrl( + Uri.parse('https://openstreetmap.org/copyright'), + ), + ), + ], + ), + ], + ), + Positioned( + top: 10, + left: 10, + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Map Status: ${_mapLoaded ? "Loaded" : "Loading..."}', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + if (_errorMessage != null) + Text( + 'Error: $_errorMessage', + style: TextStyle(color: Colors.red, fontSize: 10), + ), + ], + ), + ), + ), + ], + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: "zoom_in", + mini: true, + onPressed: () { + _mapController.move( + _mapController.camera.center, + _mapController.camera.zoom + 1, + ); + }, + child: Icon(Icons.zoom_in), + ), + SizedBox(height: 8), + FloatingActionButton( + heroTag: "zoom_out", + mini: true, + onPressed: () { + _mapController.move( + _mapController.camera.center, + _mapController.camera.zoom - 1, + ); + }, + child: Icon(Icons.zoom_out), + ), + ], + ), + ); + } + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/widgets/tree_NFT_view_widget.dart b/lib/widgets/tree_NFT_view_widget.dart index e4cba56..3747be0 100644 --- a/lib/widgets/tree_NFT_view_widget.dart +++ b/lib/widgets/tree_NFT_view_widget.dart @@ -10,50 +10,59 @@ class NewNFTWidget extends StatefulWidget { State createState() => _NewNFTWidgetState(); } - -String _formatDescription(String description) { - return description.length > 50 - ? '${description.substring(0, 50)}...' - : description; -} class _NewNFTWidgetState extends State { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.green, width: 2), - borderRadius: BorderRadius.circular(8.0), - color: Colors.white, - ), - child: Center( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Consumer(builder: (ctx, _, __) { - return Column(children: [ - Text( - 'Latitude: ${Provider.of(ctx, listen: true).getLatitude()}', - style: const TextStyle(fontSize: 20), - ), - Text( - 'Longitude: ${Provider.of(ctx, listen: true).getLongitude()}', - style: const TextStyle(fontSize: 20), - ), - Text( - 'GeoHash: ${(Provider.of(ctx, listen: true).getGeoHash())}', - style: const TextStyle(fontSize: 20), - ), - Text( - 'Species: ${Provider.of(ctx, listen: true).getSpecies()}', - style: const TextStyle(fontSize: 20), - ), - Text( - 'Description: ${formatAddress(Provider.of(ctx, listen: true).getDescription())}', - style: const TextStyle(fontSize: 20), - ), - ]); - }) - ]), + return Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 350), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.green, width: 2), + borderRadius: BorderRadius.circular(8.0), + color: Colors.white, + ), + child: Consumer( + builder: (ctx, provider, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Latitude: ${provider.getLatitude()}', + style: const TextStyle(fontSize: 18), + softWrap: true, + ), + Text( + 'Longitude: ${provider.getLongitude()}', + style: const TextStyle(fontSize: 18), + softWrap: true, + ), + Text( + 'GeoHash: ${provider.getGeoHash()}', + style: const TextStyle(fontSize: 18), + softWrap: true, + ), + Text( + 'Species: ${provider.getSpecies()}', + style: const TextStyle(fontSize: 18), + softWrap: true, + ), + Text( + 'Description: ${_formatDescription(provider.getDescription())}', + style: const TextStyle(fontSize: 18), + softWrap: true, + ), + ], + ); + }, + ), ), ); } } + +String _formatDescription(String description) { + return description.length > 80 + ? '${description.substring(0, 80)}...' + : description; +} diff --git a/pubspec.lock b/pubspec.lock index 61ff11b..14e6916 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.5" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" dart_geohash: dependency: "direct main" description: @@ -318,6 +326,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a + url: "https://pub.dev" + source: hosted + version: "8.1.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -440,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" js: dependency: transitive description: @@ -464,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" leak_tracker: dependency: transitive description: @@ -496,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" logger: dependency: "direct main" description: @@ -536,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -680,6 +728,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.1" + polylabel: + dependency: transitive + description: + name: polylabel + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" provider: dependency: "direct main" description: @@ -941,6 +1005,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" url_launcher: dependency: "direct main" description: @@ -1133,6 +1205,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.14.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" x25519: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 08fbf8c..b56d7f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: image_picker: ^1.0.4 logger: ^2.0.2+1 dart_geohash: ^2.1.0 + flutter_map: ^8.1.1 + latlong2: ^0.9.1 dev_dependencies: From 35f2e2036169eb4aeeaad45f2e65a1c3c646ad3f Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Mon, 28 Jul 2025 14:28:16 +0530 Subject: [PATCH 15/18] add: current location widget --- .env.stencil | 6 +- android/app/src/main/AndroidManifest.xml | 13 ++- lib/pages/home_page.dart | 10 +- lib/widgets/flutter_map_widget.dart | 109 ++++++++++-------- lib/widgets/location.dart | 141 +++++++++++++++++++++++ pubspec.lock | 124 +++++++++++++++++++- pubspec.yaml | 6 +- 7 files changed, 352 insertions(+), 57 deletions(-) create mode 100644 lib/widgets/location.dart diff --git a/.env.stencil b/.env.stencil index 4dc9a21..f7081d9 100644 --- a/.env.stencil +++ b/.env.stencil @@ -1 +1,5 @@ -WALLETCONNECT_PROJECT_ID= \ No newline at end of file +WALLETCONNECT_PROJECT_ID= +API_KEY= +API_SECRET= +ALCHEMY_API_KEY= +CONTRACT_ADDRESS= \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d5699d4..9c0f441 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,9 +10,16 @@ - + + + + + + + + @@ -24,6 +31,10 @@ android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher"> + { return Scaffold( body: Stack( children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: LatLng(28.7041, 77.1025), // Delhi coordinates - initialZoom: 10.0, - minZoom: 3.0, - maxZoom: 18.0, - onMapReady: () { - setState(() { - _mapLoaded = true; - }); - }, - onTap: (tapPosition, point) { - }, - ), - children: [ - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'tree_planting_protocol', - retinaMode: true, - errorTileCallback: (tile, error, stackTrace) { - setState(() { - _errorMessage = 'Tile loading error: $error'; - }); - }, - tileBuilder: (context, tileWidget, tile) { - return tileWidget; - }, - ), - MarkerLayer( - markers: [ - Marker( - point: LatLng(28.7041, 77.1025), - width: 80, - height: 80, - child: Icon( - Icons.location_pin, - color: Colors.red, - size: 40, - ), + Consumer( + builder: (context, provider, _) { + return Center( + child: FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(50, 30.15), + initialZoom: 10.0, + minZoom: 3.0, + maxZoom: 18.0, + onMapReady: () { + setState(() { + _mapLoaded = true; + }); + }, + onTap: (tapPosition, point) { + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'tree_planting_protocol', + retinaMode: true, + errorTileCallback: (tile, error, stackTrace) { + setState(() { + _errorMessage = 'Tile loading error: $error'; + }); + }, + tileBuilder: (context, tileWidget, tile) { + return tileWidget; + }, ), - ], - ), - RichAttributionWidget( - attributions: [ - TextSourceAttribution( - 'OpenStreetMap contributors', - onTap: () => launchUrl( - Uri.parse('https://openstreetmap.org/copyright'), - ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(180,67), + width: 80, + height: 80, + child: Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + RichAttributionWidget( + attributions: [ + TextSourceAttribution( + 'OpenStreetMap contributors', + onTap: () => launchUrl( + Uri.parse('https://openstreetmap.org/copyright'), + ), + ), + ], ), ], ), - ], + ); + }, + ), Positioned( top: 10, diff --git a/lib/widgets/location.dart b/lib/widgets/location.dart new file mode 100644 index 0000000..e31304f --- /dev/null +++ b/lib/widgets/location.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:location/location.dart'; + +class LocationWidget extends StatefulWidget { + const LocationWidget({Key? key}) : super(key: key); + + @override + State createState() => _LocationWidgetState(); +} + +class _LocationWidgetState extends State { + String _locationMessage = "Location not determined yet"; + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on, + size: 48, + color: Colors.blue, + ), + const SizedBox(height: 16), + Text( + 'Current Location', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 16), + _isLoading + ? const CircularProgressIndicator() + : Text( + _locationMessage, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _isLoading ? null : _getCurrentLocation, + icon: const Icon(Icons.my_location), + label: const Text('Get Location'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + ], + ), + ); + } + + Future _getCurrentLocation() async { + setState(() { + _isLoading = true; + _locationMessage = "Getting location..."; + }); + + Location location = Location(); + + try { + // Check if location service is enabled + bool serviceEnabled = await location.serviceEnabled(); + if (!serviceEnabled) { + serviceEnabled = await location.requestService(); + if (!serviceEnabled) { + setState(() { + _locationMessage = "Location services are disabled. Please enable them."; + _isLoading = false; + }); + return; + } + } + + // Check and request location permissions + PermissionStatus permissionGranted = await location.hasPermission(); + if (permissionGranted == PermissionStatus.denied) { + permissionGranted = await location.requestPermission(); + if (permissionGranted != PermissionStatus.granted) { + setState(() { + _locationMessage = "Location permissions are denied."; + _isLoading = false; + }); + return; + } + } + + // Get current location + LocationData locationData = await location.getLocation(); + + setState(() { + _locationMessage = "Latitude: ${locationData.latitude?.toStringAsFixed(6) ?? 'N/A'}\n" + "Longitude: ${locationData.longitude?.toStringAsFixed(6) ?? 'N/A'}\n" + "Accuracy: ${locationData.accuracy?.toStringAsFixed(2) ?? 'N/A'}m"; + _isLoading = false; + }); + } catch (e) { + setState(() { + _locationMessage = "Error getting location: $e"; + _isLoading = false; + }); + } + } +} + +// Example usage in your main app +class LocationApp extends StatelessWidget { + const LocationApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Location Widget Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: Scaffold( + appBar: AppBar( + title: const Text('Location Widget'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + body: const Center( + child: LocationWidget(), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 14e6916..f0db6cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -368,14 +368,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "5c23f3613f50586c0bbb2b8f970240ae66b3bd992088cf60dd5ee2e6f7dde3a8" + url: "https://pub.dev" + source: hosted + version: "9.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "4f4218f122a6978d0ad655fa3541eea74c67417440b09f0657238810d5af6bdc" + url: "https://pub.dev" + source: hosted + version: "0.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf + sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 url: "https://pub.dev" source: hosted - version: "15.2.4" + version: "16.0.0" http: dependency: "direct main" description: @@ -536,6 +584,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + location: + dependency: "direct main" + description: + name: location + sha256: "6463a242973bf247e3fb1c7722919521b98026978ee3b5177202e103a39c145e" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + location_platform_interface: + dependency: transitive + description: + name: location_platform_interface + sha256: "1e535ccc8b4a9612de4e4319871136b45d2b5d1fb0c2a8bf99687242bf7ca5f7" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + location_web: + dependency: transitive + description: + name: location_web + sha256: "613597b489beb396f658c6f4358dd383c5ed0a1402d95e287642a5f2d8171cb0" + url: "https://pub.dev" + source: hosted + version: "5.0.3" logger: dependency: "direct main" description: @@ -696,6 +768,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b56d7f2..2a4991f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: url_launcher: ^6.2.1 provider: ^6.1.5 shared_preferences: ^2.2.2 - go_router: ^15.2.4 + go_router: ^16.0.0 cupertino_icons: ^1.0.8 flutter_dotenv: ^5.1.0 image_picker: ^1.0.4 @@ -48,6 +48,10 @@ dependencies: dart_geohash: ^2.1.0 flutter_map: ^8.1.1 latlong2: ^0.9.1 + geolocator: ^9.0.2 + location: ^7.0.0 + permission_handler: ^12.0.1 + # fluttertoast: ^8.2.8 dev_dependencies: From c5aae303b56e271e7e6d11b7937208bd5156f0c9 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Tue, 29 Jul 2025 01:56:33 +0530 Subject: [PATCH 16/18] add: preview widget with map integration --- lib/main.dart | 8 + lib/pages/counter_page.dart | 1 - lib/pages/mint_nft/mint_nft_coordinates.dart | 673 ++++++++++++++++-- lib/pages/mint_nft/mint_nft_details.dart | 369 ++++++++-- lib/pages/mint_nft/mint_nft_images.dart | 404 ++++++----- lib/pages/mint_nft/submit_nft_page.dart | 24 + lib/utils/services/get_current_location.dart | 146 ++++ lib/widgets/flutter_map_widget.dart | 663 ++++++++++++++--- lib/widgets/location.dart | 6 - .../tree_nft_view_details_with_map.dart | 187 +++++ 10 files changed, 2080 insertions(+), 401 deletions(-) create mode 100644 lib/pages/mint_nft/submit_nft_page.dart create mode 100644 lib/utils/services/get_current_location.dart create mode 100644 lib/widgets/tree_nft_view_details_with_map.dart diff --git a/lib/main.dart b/lib/main.dart index 328ce75..d2180c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_images.dart'; +import 'package:tree_planting_protocol/pages/mint_nft/submit_nft_page.dart'; import 'package:tree_planting_protocol/pages/settings_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; @@ -81,6 +82,13 @@ class MyApp extends StatelessWidget { return const MultipleImageUploadPage(); }, ), + GoRoute( + path: 'submit-nft', // This will be /trees/details + name: '${RouteConstants.mintNft}_submit', + builder: (BuildContext context, GoRouterState state) { + return const SubmitNFTPage(); + }, + ), ]), GoRoute( path: RouteConstants.allTreesPath, diff --git a/lib/pages/counter_page.dart b/lib/pages/counter_page.dart index 242930b..15e0cee 100644 --- a/lib/pages/counter_page.dart +++ b/lib/pages/counter_page.dart @@ -66,7 +66,6 @@ class _CounterPageState extends State { ); setState(() { - // The result is a List, and getCount returns a single uint256 currentCount = result.isNotEmpty ? result[0].toString() : '0'; isLoading = false; }); diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index c8373e5..709bd01 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -6,6 +7,7 @@ import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/flutter_map_widget.dart'; import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; +import 'package:tree_planting_protocol/utils/services/get_current_location.dart'; import 'package:dart_geohash/dart_geohash.dart'; class MintNftCoordinatesPage extends StatefulWidget { @@ -19,98 +21,657 @@ class _MintNftCoordinatesPageState extends State { final latitudeController = TextEditingController(); final longitudeController = TextEditingController(); var geoHasher = GeoHasher(); + + final LocationService _locationService = LocationService(); + Timer? _locationTimer; + bool _isLoadingLocation = true; + String _locationStatus = "Getting current location..."; + bool _userHasManuallySetCoordinates = false; + bool _isInitialLocationSet = false; + + @override + void initState() { + super.initState(); + _initializeLocation(); + _startLocationUpdates(); + _setupTextFieldListeners(); + } + + void _setupTextFieldListeners() { + latitudeController.addListener(() { + if (_isInitialLocationSet && !_isLoadingLocation) { + _userHasManuallySetCoordinates = true; + } + }); + + longitudeController.addListener(() { + if (_isInitialLocationSet && !_isLoadingLocation) { + _userHasManuallySetCoordinates = true; + } + }); + } + + Future _initializeLocation() async { + await _getCurrentLocation(); + } + + void _startLocationUpdates() { + _locationTimer = Timer.periodic(const Duration(seconds: 5), (timer) { + if (!_userHasManuallySetCoordinates) { + _getCurrentLocation(); + } + }); + } + + Future _getCurrentLocation() async { + try { + LocationInfo locationInfo = await _locationService.getCurrentLocationWithTimeout( + timeout: const Duration(seconds: 10), + ); + + if (mounted && locationInfo.isValid) { + final provider = Provider.of(context, listen: false); + + setState(() { + _isLoadingLocation = false; + _locationStatus = "Location updated"; + }); + provider.setLatitude(locationInfo.latitude!); + provider.setLongitude(locationInfo.longitude!); + if (!_userHasManuallySetCoordinates) { + latitudeController.text = locationInfo.latitude!.toStringAsFixed(6); + longitudeController.text = locationInfo.longitude!.toStringAsFixed(6); + } + if (!_isInitialLocationSet) { + _isInitialLocationSet = true; + } + } + } on LocationException catch (e) { + if (mounted) { + setState(() { + _isLoadingLocation = false; + _locationStatus = "Location error: ${e.message}"; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoadingLocation = false; + _locationStatus = "Failed to get location: $e"; + }); + } + } + } + + void _onMapLocationSelected(double lat, double lng) { + setState(() { + _userHasManuallySetCoordinates = true; + latitudeController.text = lat.toStringAsFixed(6); + longitudeController.text = lng.toStringAsFixed(6); + }); + } void submitCoordinates() { final latitude = latitudeController.text; final longitude = longitudeController.text; - final geohash = geoHasher.encode( - double.parse(latitude), - double.parse(longitude), - precision: 12, - ); if (latitude.isEmpty || longitude.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please enter both latitude and longitude.")), + _showCustomSnackBar( + "Please enter both latitude and longitude.", + isError: true, ); return; } - Provider.of(context, listen: false) - .setLatitude(double.parse(latitude)); - Provider.of(context, listen: false) - .setLongitude(double.parse(longitude)); - Provider.of(context, listen: false) - .setGeoHash(geohash); - - latitudeController.clear(); - longitudeController.clear(); + try { + final lat = double.parse(latitude); + final lng = double.parse(longitude); + if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { + _showCustomSnackBar( + "Please enter valid coordinates. Latitude: -90 to 90, Longitude: -180 to 180", + isError: true, + ); + return; + } + + final geohash = geoHasher.encode(lat, lng, precision: 12); + + Provider.of(context, listen: false).setLatitude(lat); + Provider.of(context, listen: false).setLongitude(lng); + Provider.of(context, listen: false).setGeoHash(geohash); + + latitudeController.clear(); + longitudeController.clear(); + + _showCustomSnackBar("Coordinates submitted successfully!"); + context.push(RouteConstants.mintNftDetailsPath); + } catch (e) { + _showCustomSnackBar( + "Please enter valid numeric coordinates.", + isError: true, + ); + } + } + + Future _refreshLocation() async { + setState(() { + _isLoadingLocation = true; + _locationStatus = "Refreshing location..."; + _userHasManuallySetCoordinates = false; + }); + await _getCurrentLocation(); + } + + void _useCurrentLocation() { + setState(() { + _userHasManuallySetCoordinates = false; + _isLoadingLocation = true; + _locationStatus = "Getting current location..."; + }); + _getCurrentLocation(); + } + + void _showCustomSnackBar(String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Coordinates submitted successfully.")), + SnackBar( + content: Row( + children: [ + Icon( + isError ? Icons.error_outline : Icons.check_circle_outline, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + backgroundColor: isError + ? Colors.red.shade400 + : const Color(0xFF1CD381), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(16), + ), ); - context.push(RouteConstants.mintNftDetailsPath); } @override Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + return BaseScaffold( title: "Mint NFT Coordinates", body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const NewNFTWidget(), - const SizedBox(height: 20), - const SizedBox( - height: 300, - width: 350, - child: CoordinatesMap() + padding: EdgeInsets.symmetric( + horizontal: screenWidth * 0.05, + vertical: 20, + ), + child: Column( + children: [ + // Form Section + _buildFormSection(screenWidth, screenHeight), + + const SizedBox(height: 32), + + // Preview Section + _buildPreviewSection(), + ], + ), + ), + ); + } + + Widget _buildFormSection(double screenWidth, double screenHeight) { + return Container( + width: double.infinity, + constraints: BoxConstraints(maxWidth: screenWidth * 0.92), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF1CD381).withOpacity(0.05), + const Color(0xFFFAEB96).withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: const Color(0xFF1CD381).withOpacity(0.15), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Form Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF1CD381), + const Color(0xFF1CD381).withOpacity(0.8), + ], ), - const Text( - "Enter your coordinates", - style: TextStyle(fontSize: 30), - textAlign: TextAlign.center, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), ), - const SizedBox(height: 20), - TextField( - controller: latitudeController, - decoration: const InputDecoration( - labelText: "Latitude", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon( + Icons.location_on, + color: Colors.white, + size: 28, + ), + ), + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Tree Location', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + 'Mark where your tree is planted', + style: TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], + ), ), + ], + ), + ), + + // Content + Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + // Location Status + _buildLocationStatus(), + + const SizedBox(height: 24), + + // Map Section + _buildMapSection(screenHeight), + + const SizedBox(height: 24), + + // Instructions + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFAEB96).withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFFAEB96).withOpacity(0.5), + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: const Color(0xFF1CD381), + size: 20, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Tap on the map or enter coordinates manually below', + style: TextStyle( + fontSize: 14, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Coordinate Fields + Row( + children: [ + Expanded( + child: _buildCoordinateField( + controller: latitudeController, + label: 'Latitude', + icon: Icons.straighten, + hint: '-90 to 90', + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCoordinateField( + controller: longitudeController, + label: 'Longitude', + icon: Icons.straighten, + hint: '-180 to 180', + ), + ), + ], + ), + + const SizedBox(height: 32), + + // Submit Button + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: submitCoordinates, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1CD381), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + shadowColor: const Color(0xFF1CD381).withOpacity(0.3), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Continue', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + ), + child: const Icon( + Icons.arrow_forward, + size: 18, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLocationStatus() { + final isManual = _userHasManuallySetCoordinates; + final isLoading = _isLoadingLocation; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isLoading + ? Colors.orange.withOpacity(0.3) + : (isManual ? const Color(0xFFFAEB96) : const Color(0xFF1CD381).withOpacity(0.3)), + width: 2, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isLoading + ? Colors.orange.withOpacity(0.1) + : (isManual ? const Color(0xFFFAEB96).withOpacity(0.3) : const Color(0xFF1CD381).withOpacity(0.1)), + borderRadius: BorderRadius.circular(8), + ), + child: isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Icon( + isManual ? Icons.edit_location : Icons.my_location, + size: 20, + color: isManual ? Colors.orange.shade700 : const Color(0xFF1CD381), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isManual ? "Manual Coordinates" : _locationStatus, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isLoading + ? Colors.orange.shade700 + : (isManual ? Colors.orange.shade700 : const Color(0xFF1CD381)), + ), + ), + if (isManual) + const Text( + "Using your entered coordinates", + style: TextStyle( + fontSize: 12, + color: Colors.black54, + ), + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _refreshLocation, + icon: const Icon(Icons.refresh, size: 20), + style: IconButton.styleFrom( + backgroundColor: const Color(0xFF1CD381).withOpacity(0.1), + foregroundColor: const Color(0xFF1CD381), + ), + ), + if (isManual) ...[ + const SizedBox(width: 8), + TextButton.icon( + onPressed: _useCurrentLocation, + icon: const Icon(Icons.my_location, size: 16), + label: const Text("Auto"), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF1CD381), + backgroundColor: const Color(0xFF1CD381).withOpacity(0.1), + ), + ), + ], + ], + ), + ], + ), + ); + } + + Widget _buildMapSection(double screenHeight) { + final mapHeight = (screenHeight * 0.35).clamp(250.0, 350.0); + + return Container( + height: mapHeight, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: CoordinatesMap( + onLocationSelected: _onMapLocationSelected, + lat: Provider.of(context).getLatitude().toDouble(), + lng: Provider.of(context).getLongitude().toDouble(), + ), + ); + } + + Widget _buildCoordinateField({ + required TextEditingController controller, + required String label, + required IconData icon, + required String hint, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: const Color(0xFF1CD381).withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + icon, + color: const Color(0xFF1CD381), + size: 14, + ), + ), + const SizedBox(width: 6), + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF1CD381), + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFFAEB96).withOpacity(0.5), + width: 2, + ), + ), + child: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: 12, ), - const SizedBox(height: 10), - TextField( - controller: longitudeController, - decoration: const InputDecoration( - labelText: "Longitude", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + filled: true, + fillColor: Colors.transparent, + ), + onTap: () { + _userHasManuallySetCoordinates = true; + }, + ), + ), + ], + ); + } + + Widget _buildPreviewSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFFAEB96).withOpacity(0.3), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.preview, + color: const Color(0xFF1CD381), + size: 20, ), ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: submitCoordinates, - child: const Text( - "Next", - style: TextStyle(fontSize: 20, color: Colors.white), + const SizedBox(width: 12), + const Text( + 'Live Preview', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1CD381), ), ), ], ), ), - ), + const NewNFTWidget(), + ], ); } @override void dispose() { + _locationTimer?.cancel(); latitudeController.dispose(); longitudeController.dispose(); super.dispose(); } -} +} \ No newline at end of file diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 65d0486..4be09e4 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -4,10 +4,12 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; -import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; +import 'package:tree_planting_protocol/widgets/flutter_map_widget.dart'; +import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; +import 'package:tree_planting_protocol/widgets/tree_nft_view_details_with_map.dart'; class MintNftDetailsPage extends StatefulWidget { - const MintNftDetailsPage ({super.key}); + const MintNftDetailsPage({super.key}); @override State createState() => _MintNftCoordinatesPageState(); @@ -22,82 +24,359 @@ class _MintNftCoordinatesPageState extends State { final species = speciesController.text; if (description.isEmpty || species.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please enter both desriptiona and species.")), + _showCustomSnackBar( + "Please enter both description and species.", + isError: true, ); return; } + Provider.of(context, listen: false) .setDescription(description); Provider.of(context, listen: false) .setSpecies(species); - speciesController.clear(); - descriptionController.clear(); + + _showCustomSnackBar("Details submitted successfully!"); + context.push(RouteConstants.mintNftImagesPath); + } + void _showCustomSnackBar(String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Details submitted successfully.")), + SnackBar( + content: Row( + children: [ + Icon( + isError ? Icons.error_outline : Icons.check_circle_outline, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + backgroundColor: isError + ? Colors.red.shade400 + : const Color(0xFF1CD381), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(16), + ), ); - context.push(RouteConstants.mintNftImagesPath); } @override Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + return BaseScaffold( title: "NFT Details", - body: Center( - child: SingleChildScrollView( - child: - Column( - mainAxisAlignment: MainAxisAlignment.center, + body: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: screenWidth * 0.05, + vertical: 20, + ), + child: Column( + children: [ + _buildFormSection(screenWidth), + + const SizedBox(height: 32), + _buildPreviewSection(), + ], + ), + ), + ); + } + + Widget _buildFormSection(double screenWidth) { + return Container( + width: double.infinity, + constraints: BoxConstraints(maxWidth: screenWidth * 0.92), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF1CD381).withOpacity(0.05), + const Color(0xFFFAEB96).withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: const Color(0xFF1CD381).withOpacity(0.15), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF1CD381), + const Color(0xFF1CD381).withOpacity(0.8), + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Row( children: [ - const NewNFTWidget(), - const SizedBox(height: 20), - const Text( - "Enter NFT Details", - style: TextStyle(fontSize: 30), - textAlign: TextAlign.center, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon( + Icons.edit_note, + color: Colors.white, + size: 28, + ), ), - const SizedBox(height: 20), - TextField( - minLines: 4, - maxLines: 8, - controller: descriptionController, - decoration: const InputDecoration( - labelText: "Description", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300, maxHeight: 200), - alignLabelWithHint: true, + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'NFT Details Form', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Text( + 'Tell us about your tree', + style: TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], ), ), - const SizedBox(height: 10), - TextField( + ], + ), + ), + + // Form Fields + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFormField( controller: speciesController, - decoration: const InputDecoration( - labelText: "Species", - border: OutlineInputBorder(), - constraints: BoxConstraints(maxWidth: 300), - ), + label: 'Tree Species', + hint: 'e.g., Oak, Pine, Maple...', + icon: Icons.eco, + maxLines: 1, ), + const SizedBox(height: 20), - ElevatedButton( - onPressed: submitDetails, - child: const Text( - "Next", - style: TextStyle(fontSize: 20, color: Colors.white), + + _buildFormField( + controller: descriptionController, + label: 'Description', + hint: 'Describe your tree planting experience...', + icon: Icons.description, + maxLines: 5, + minLines: 3, + ), + + const SizedBox(height: 32), + + // Submit Button + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: submitDetails, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1CD381), + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + shadowColor: const Color(0xFF1CD381).withOpacity(0.3), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Continue', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + ), + child: const Icon( + Icons.arrow_forward, + size: 18, + ), + ), + ], + ), ), - ) + ), ], ), - ), + ), + ], ), ); } + Widget _buildFormField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + int? minLines, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFF1CD381).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: const Color(0xFF1CD381), + size: 18, + ), + ), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF1CD381), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFFAEB96).withOpacity(0.5), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF1CD381).withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: controller, + maxLines: maxLines, + minLines: minLines, + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + height: 1.4, + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: 14, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.all(20), + filled: true, + fillColor: Colors.transparent, + ), + ), + ), + ], + ); + } + + Widget _buildPreviewSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFFAEB96).withOpacity(0.3), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.preview, + color: const Color(0xFF1CD381), + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + 'Live Preview', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1CD381), + ), + ), + ], + ), + ), + const NewNFTMapWidget(), + ], + ); + } + @override void dispose() { descriptionController.dispose(); speciesController.dispose(); super.dispose(); } -} +} \ No newline at end of file diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart index 4f003b5..afc570d 100644 --- a/lib/pages/mint_nft/mint_nft_images.dart +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -1,17 +1,20 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; -import 'package:tree_planting_protocol/widgets/tree_NFT_view_widget.dart'; +import 'package:tree_planting_protocol/widgets/tree_nft_view_details_with_map.dart'; class MultipleImageUploadPage extends StatefulWidget { - const MultipleImageUploadPage({Key? key}) : super(key: key); + const MultipleImageUploadPage({super.key}); @override - _MultipleImageUploadPageState createState() => _MultipleImageUploadPageState(); + // ignore: library_private_types_in_public_api + _MultipleImageUploadPageState createState() => + _MultipleImageUploadPageState(); } class _MultipleImageUploadPageState extends State { @@ -83,7 +86,7 @@ class _MultipleImageUploadPageState extends State { _uploadingIndex = -1; }); provider.setInitialPhotos(_uploadedHashes); - + if (newHashes.isNotEmpty) { _showSnackBar('Successfully uploaded ${newHashes.length} images'); } @@ -159,201 +162,139 @@ class _MultipleImageUploadPageState extends State { @override Widget build(BuildContext context) { return BaseScaffold( - body: SingleChildScrollView( - child: - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const NewNFTWidget(), - const SizedBox(height: 20), - Row( + title: "Mint Tree NFT", + body: Column( + children: [ + Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6, + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: _isUploading ? null : _pickImages, - icon: const Icon(Icons.photo_library), - label: const Text('Select Images'), - ), + Text( + 'Upload Images', + style: Theme.of(context).textTheme.titleLarge, ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton.icon( - onPressed: (_selectedImages.isEmpty || _isUploading) - ? null - : _uploadAllImages, - icon: const Icon(Icons.cloud_upload), - label: const Text('Upload All'), - ), - ), - ], - ), - - const SizedBox(height: 16), - if (_isUploading) - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 8), - Text( - _uploadingIndex >= 0 - ? 'Uploading image ${_uploadingIndex + 1} of ${_selectedImages.length}...' - : 'Uploading...', - style: Theme.of(context).textTheme.bodyMedium, + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isUploading ? null : _pickImages, + icon: const Icon(Icons.photo_library), + label: const Text('Select Images'), ), - ], - ), - ), - ), - - const SizedBox(height: 16), - if (_selectedImages.isNotEmpty) ...[ - Text( - 'Selected Images (${_selectedImages.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _selectedImages.length, - itemBuilder: (context, index) { - return Container( - width: 120, - margin: const EdgeInsets.only(right: 8), - child: Card( - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file( - _selectedImages[index], - width: 120, - height: 80, - fit: BoxFit.cover, - ), - ), - - Positioned( - top: 4, - right: 4, - child: GestureDetector( - onTap: () => _removeImage(index), - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.close, - color: Colors.white, - size: 16, - ), - ), - ), - ), - Positioned( - bottom: 4, - left: 4, - right: 4, - child: SizedBox( - height: 28, - child: ElevatedButton( - onPressed: (_isUploading && _uploadingIndex == index) - ? null - : () => _uploadSingleImage(index), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - child: (_isUploading && _uploadingIndex == index) - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.upload, size: 16), - ), - ), - ), - ], - ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton.icon( + onPressed: (_selectedImages.isEmpty || _isUploading) + ? null + : _uploadAllImages, + icon: const Icon(Icons.cloud_upload), + label: const Text('Upload All'), ), - ); - }, + ), + ], ), - ), - const SizedBox(height: 16), - ], - if (_uploadedHashes.isNotEmpty) ...[ - Text( - 'Uploaded Images (${_uploadedHashes.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - ], - Expanded( - child: _uploadedHashes.isEmpty - ? Center( + const SizedBox(height: 16), + if (_isUploading) + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.cloud_off, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), + const CircularProgressIndicator(), + const SizedBox(height: 8), Text( - 'No images uploaded yet', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), + _uploadingIndex >= 0 + ? 'Uploading image ${_uploadingIndex + 1} of ${_selectedImages.length}...' + : 'Uploading...', + style: Theme.of(context).textTheme.bodyMedium, ), ], ), - ) - : ListView.builder( - itemCount: _uploadedHashes.length, + ), + ), + const SizedBox(height: 16), + if (_selectedImages.isNotEmpty) ...[ + Text( + 'Selected Images (${_selectedImages.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _selectedImages.length, itemBuilder: (context, index) { - return Card( - margin: const EdgeInsets.only(bottom: 8), - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green, - child: Text('${index + 1}'), - ), - title: Text( - 'Image ${index + 1}', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - _uploadedHashes[index], - style: const TextStyle(fontSize: 12), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, + return Container( + width: 120, + margin: const EdgeInsets.only(right: 8), + child: Card( + child: Stack( children: [ - IconButton( - icon: const Icon(Icons.open_in_new), - onPressed: () { - // You can implement opening the IPFS link here - _showSnackBar('IPFS Hash: ${_uploadedHashes[index]}'); - }, - tooltip: 'View on IPFS', + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + _selectedImages[index], + width: 120, + height: 80, + fit: BoxFit.cover, + ), ), - IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () => _removeUploadedHash(index), - tooltip: 'Remove', + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => _removeImage(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), + ), + Positioned( + bottom: 4, + left: 4, + right: 4, + child: SizedBox( + height: 28, + child: ElevatedButton( + onPressed: (_isUploading && + _uploadingIndex == index) + ? null + : () => _uploadSingleImage(index), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8), + ), + child: (_isUploading && + _uploadingIndex == index) + ? const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator( + strokeWidth: 2), + ) + : const Icon(Icons.upload, + size: 16), + ), + ), ), ], ), @@ -361,11 +302,100 @@ class _MultipleImageUploadPageState extends State { ); }, ), + ), + const SizedBox(height: 16), + ], + if (_uploadedHashes.isNotEmpty) + Text( + 'Uploaded Images (${_uploadedHashes.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + context.push('/mint-nft/submit-nft'); + }, + child: Text( + "Submit NFT", + style: Theme.of(context).textTheme.titleMedium, + ) + ), + NewNFTMapWidget(), + ], ), - ], + ), ), ), + Expanded( + child: _uploadedHashes.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud_off, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + 'No images uploaded yet', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16, + ), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: _uploadedHashes.length, + itemBuilder: (context, index) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: Colors.green, + child: Text('${index + 1}'), + ), + title: Text( + 'Image ${index + 1}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + _uploadedHashes[index], + style: const TextStyle(fontSize: 12), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.open_in_new), + onPressed: () { + // You can implement opening the IPFS link here + _showSnackBar( + 'IPFS Hash: ${_uploadedHashes[index]}'); + }, + tooltip: 'View on IPFS', + ), + IconButton( + icon: + const Icon(Icons.delete, color: Colors.red), + onPressed: () => _removeUploadedHash(index), + tooltip: 'Remove', + ), + ], + ), + ), + ); + }, + ), + ), + ], ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/mint_nft/submit_nft_page.dart b/lib/pages/mint_nft/submit_nft_page.dart new file mode 100644 index 0000000..9d1b2eb --- /dev/null +++ b/lib/pages/mint_nft/submit_nft_page.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/tree_nft_view_details_with_map.dart'; + + + +class SubmitNFTPage extends StatefulWidget { + const SubmitNFTPage({super.key}); + + @override + State createState() => _SubmitNFTPageState(); +} + +class _SubmitNFTPageState extends State { + @override + Widget build(BuildContext context) { + return const BaseScaffold( + title: "Submit NFT", + body: NewNFTMapWidget() + ); + } +} \ No newline at end of file diff --git a/lib/utils/services/get_current_location.dart b/lib/utils/services/get_current_location.dart new file mode 100644 index 0000000..8cc42da --- /dev/null +++ b/lib/utils/services/get_current_location.dart @@ -0,0 +1,146 @@ +import 'package:location/location.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; + + +class LocationException implements Exception { + final String message; + final LocationErrorType type; + + const LocationException(this.message, this.type); + + @override + String toString() => 'LocationException: $message'; +} + +enum LocationErrorType { + serviceDisabled, + permissionDenied, + locationUnavailable, + unknown, +} +class LocationInfo { + final double? latitude; + final double? longitude; + final double? accuracy; + final double? altitude; + final double? speed; + final double? speedAccuracy; + final double? heading; + final int? time; + + const LocationInfo({ + this.latitude, + this.longitude, + this.accuracy, + this.altitude, + this.speed, + this.speedAccuracy, + this.heading, + this.time, + }); + + factory LocationInfo.fromLocationData(LocationData data) { + return LocationInfo( + latitude: data.latitude, + longitude: data.longitude, + accuracy: data.accuracy, + altitude: data.altitude, + speed: data.speed, + speedAccuracy: data.speedAccuracy, + heading: data.heading, + time: data.time?.toInt(), + ); + } + String get formattedLocation { + return "Latitude: ${latitude?.toStringAsFixed(6) ?? 'N/A'}\n" + "Longitude: ${longitude?.toStringAsFixed(6) ?? 'N/A'}\n" + "Accuracy: ${accuracy?.toStringAsFixed(2) ?? 'N/A'}m"; + } + bool get isValid => latitude != null && longitude != null; + + @override + String toString() { + return 'LocationInfo(lat: $latitude, lng: $longitude, accuracy: $accuracy)'; + } +} + +class LocationService { + static final LocationService _instance = LocationService._internal(); + factory LocationService() => _instance; + LocationService._internal(); + + final Location _location = Location(); + Future getCurrentLocation() async { + try { + bool serviceEnabled = await _location.serviceEnabled(); + if (!serviceEnabled) { + serviceEnabled = await _location.requestService(); + if (!serviceEnabled) { + const error = "Location services are disabled. Please enable them in settings."; + logger.e(error); + throw const LocationException(error, LocationErrorType.serviceDisabled); + } + } + + PermissionStatus permissionGranted = await _location.hasPermission(); + if (permissionGranted == PermissionStatus.denied) { + permissionGranted = await _location.requestPermission(); + if (permissionGranted != PermissionStatus.granted) { + const error = "Location permissions are denied. Please grant location access."; + logger.e(error); + throw const LocationException(error, LocationErrorType.permissionDenied); + } + } + LocationData locationData = await _location.getLocation(); + + if (locationData.latitude == null || locationData.longitude == null) { + const error = "Unable to retrieve valid location coordinates."; + logger.e(error); + throw const LocationException(error, LocationErrorType.locationUnavailable); + } + + logger.i("Location retrieved successfully: ${locationData.latitude}, ${locationData.longitude}"); + return LocationInfo.fromLocationData(locationData); + + } catch (e) { + if (e is LocationException) { + rethrow; + } + + final error = "Error getting location: $e"; + logger.e(error); + throw LocationException(error, LocationErrorType.unknown); + } + } + + Future getCurrentLocationWithTimeout({ + Duration timeout = const Duration(seconds: 30), + }) async { + return Future.any([ + getCurrentLocation(), + Future.delayed(timeout, () { + throw const LocationException( + "Location request timed out", + LocationErrorType.locationUnavailable, + ); + }), + ]); + } + Stream getLocationStream() { + return _location.onLocationChanged.map((locationData) { + return LocationInfo.fromLocationData(locationData); + }); + } + Future isLocationServiceAvailable() async { + try { + bool serviceEnabled = await _location.serviceEnabled(); + if (!serviceEnabled) return false; + + PermissionStatus permissionGranted = await _location.hasPermission(); + return permissionGranted == PermissionStatus.granted; + } catch (e) { + logger.e("Error checking location service availability: $e"); + return false; + } + } +} \ No newline at end of file diff --git a/lib/widgets/flutter_map_widget.dart b/lib/widgets/flutter_map_widget.dart index a048b09..8078d90 100644 --- a/lib/widgets/flutter_map_widget.dart +++ b/lib/widgets/flutter_map_widget.dart @@ -3,10 +3,11 @@ import 'package:latlong2/latlong.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; class CoordinatesMap extends StatefulWidget { - const CoordinatesMap({super.key}); + final Function(double lat, double lng)? onLocationSelected; + + const CoordinatesMap({Key? key, this.onLocationSelected, required double lat, required double lng}) : super(key: key); @override State createState() => _CoordinatesMapState(); @@ -15,7 +16,12 @@ class CoordinatesMap extends StatefulWidget { class _CoordinatesMapState extends State { late MapController _mapController; bool _mapLoaded = false; + bool _hasError = false; String? _errorMessage; + + // Default location (you can change this to any default location) + static const double _defaultLat = 28.9845; // Example: Roorkee, India + static const double _defaultLng = 77.8956; @override void initState() { @@ -25,127 +31,572 @@ class _CoordinatesMapState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - Consumer( - builder: (context, provider, _) { - return Center( - child: FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: LatLng(50, 30.15), - initialZoom: 10.0, - minZoom: 3.0, - maxZoom: 18.0, - onMapReady: () { - setState(() { - _mapLoaded = true; - }); - }, - onTap: (tapPosition, point) { - }, + return Consumer( + builder: (context, provider, _) { + final double latitude = provider.getLatitude() ?? _defaultLat; + final double longitude = provider.getLongitude() ?? _defaultLng; + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _hasError ? _buildErrorWidget() : _buildMapWidget(latitude, longitude), + ), + ); + }, + ); + } + + Widget _buildErrorWidget() { + return Container( + color: Colors.grey[100], + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.map_outlined, + size: 60, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + "Map Unavailable", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "Please use the coordinate fields below", + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _hasError = false; + _errorMessage = null; + }); + }, + child: const Text("Retry"), + ), + ], + ), + ), + ); + } + + Widget _buildMapWidget(double latitude, double longitude) { + return Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(latitude, longitude), + initialZoom: 15.0, + minZoom: 3.0, + maxZoom: 18.0, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all, + ), + onMapReady: () { + setState(() { + _mapLoaded = true; + }); + }, + onTap: (tapPosition, point) { + final provider = Provider.of(context, listen: false); + provider.setLatitude(point.latitude); + provider.setLongitude(point.longitude); + if (widget.onLocationSelected != null) { + widget.onLocationSelected!(point.latitude, point.longitude); + } + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'tree_planting_protocol', + errorTileCallback: (tile, error, stackTrace) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = 'Network connection issue'; + }); + } + }, + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(latitude, longitude), + width: 80, + height: 80, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), ), + ], + ), + ], + ), + if (!_mapLoaded) + Container( + color: Colors.white.withOpacity(0.8), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'tree_planting_protocol', - retinaMode: true, - errorTileCallback: (tile, error, stackTrace) { - setState(() { - _errorMessage = 'Tile loading error: $error'; - }); - }, - tileBuilder: (context, tileWidget, tile) { - return tileWidget; - }, - ), - MarkerLayer( - markers: [ - Marker( - point: LatLng(180,67), - width: 80, - height: 80, - child: Icon( - Icons.location_pin, - color: Colors.red, - size: 40, + CircularProgressIndicator(), + SizedBox(height: 16), + Text("Loading map..."), + ], + ), + ), + ), + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + Positioned( + right: 8, + top: 50, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom < 18.0) { + _mapController.move( + _mapController.camera.center, + currentZoom + 1, + ); + } + }, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.add, + color: Colors.black87, + size: 20, + ), ), ), - ], - ), - RichAttributionWidget( - attributions: [ - TextSourceAttribution( - 'OpenStreetMap contributors', - onTap: () => launchUrl( - Uri.parse('https://openstreetmap.org/copyright'), + ), + Container( + height: 1, + color: Colors.grey[300], + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom > 3.0) { + _mapController.move( + _mapController.camera.center, + currentZoom - 1, + ); + } + }, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.remove, + color: Colors.black87, + size: 20, + ), ), ), - ], - ), - ], + ), + ], + ), ), - ); - }, - + ], ), - Positioned( - top: 10, - left: 10, - child: Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(4), + ), + Positioned( + bottom: 8, + left: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.8), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + "Tap to set location • Use zoom buttons or pinch to zoom", + style: TextStyle( + color: Colors.white, + fontSize: 11, ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + } + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } +} + + +class StaticDisplayMap extends StatefulWidget { + final Function(double lat, double lng)? onLocationSelected; + final double lat; + final double lng; + + const StaticDisplayMap({Key? key, this.onLocationSelected, required this.lat, required this.lng}) : super(key: key); + + @override + State createState() => _StaticDisplayMapState(); +} + +class _StaticDisplayMapState extends State { + late MapController _mapController; + bool _mapLoaded = false; + bool _hasError = false; + String? _errorMessage; + static const double _defaultLat = 28.9845; // Example: Roorkee, India + static const double _defaultLng = 77.8956; + + @override + void initState() { + super.initState(); + _mapController = MapController(); + } + double _sanitizeCoordinate(double value, double defaultValue) { + if (value.isNaN || value.isInfinite || value == double.infinity || value == double.negativeInfinity) { + print('Invalid coordinate detected: $value, using default: $defaultValue'); + return defaultValue; + } + return value; + } + + @override + Widget build(BuildContext context) { + double latitude = _sanitizeCoordinate(widget.lat, _defaultLat); + double longitude = _sanitizeCoordinate(widget.lng, _defaultLng); + latitude = latitude.clamp(-90.0, 90.0); + longitude = longitude.clamp(-180.0, 180.0); + + // Debug print to see what values we're getting + print('StaticDisplayMap - lat: $latitude, lng: $longitude'); + print('Widget.lat: ${widget.lat}, Widget.lng: ${widget.lng}'); + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _hasError ? _buildErrorWidget() : _buildMapWidget(latitude, longitude), + ), + ); + } + + Widget _buildErrorWidget() { + return Container( + color: Colors.grey[100], + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.map_outlined, + size: 60, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + "Map Unavailable", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "Please use the coordinate fields below", + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _hasError = false; + _errorMessage = null; + }); + }, + child: const Text("Retry"), + ), + ], + ), + ), + ); + } + + Widget _buildMapWidget(double latitude, double longitude) { + // Final safety check before creating LatLng + if (latitude.isNaN || latitude.isInfinite || longitude.isNaN || longitude.isInfinite) { + print('ERROR: Invalid coordinates in _buildMapWidget - lat: $latitude, lng: $longitude'); + latitude = _defaultLat; + longitude = _defaultLng; + } + + return Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(latitude, longitude), + initialZoom: 15.0, + minZoom: 3.0, + maxZoom: 18.0, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all, + ), + onMapReady: () { + setState(() { + _mapLoaded = true; + }); + }, + onTap: (tapPosition, point) { + // For static display, you might want to disable tap or handle differently + if (widget.onLocationSelected != null) { + widget.onLocationSelected!(point.latitude, point.longitude); + } + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'tree_planting_protocol', + errorTileCallback: (tile, error, stackTrace) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = 'Network connection issue'; + }); + } + }, + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(latitude, longitude), + width: 80, + height: 80, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], + ), + if (!_mapLoaded) + Container( + color: Colors.white.withOpacity(0.8), + child: const Center( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Map Status: ${_mapLoaded ? "Loaded" : "Loading..."}', - style: TextStyle(color: Colors.white, fontSize: 12), - ), - if (_errorMessage != null) - Text( - 'Error: $_errorMessage', - style: TextStyle(color: Colors.red, fontSize: 10), - ), + CircularProgressIndicator(), + SizedBox(height: 16), + Text("Loading map..."), ], ), ), ), - ], - ), - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - heroTag: "zoom_in", - mini: true, - onPressed: () { - _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom + 1, - ); - }, - child: Icon(Icons.zoom_in), + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), ), - SizedBox(height: 8), - FloatingActionButton( - heroTag: "zoom_out", - mini: true, - onPressed: () { - _mapController.move( - _mapController.camera.center, - _mapController.camera.zoom - 1, - ); - }, - child: Icon(Icons.zoom_out), + ), + Positioned( + right: 8, + top: 50, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom < 18.0) { + _mapController.move( + _mapController.camera.center, + currentZoom + 1, + ); + } + }, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.add, + color: Colors.black87, + size: 20, + ), + ), + ), + ), + Container( + height: 1, + color: Colors.grey[300], + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom > 3.0) { + _mapController.move( + _mapController.camera.center, + currentZoom - 1, + ); + } + }, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.remove, + color: Colors.black87, + size: 20, + ), + ), + ), + ), + ], + ), + ), + ], ), - ], - ), + ), + Positioned( + bottom: 8, + left: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.8), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + "Static display • Use zoom buttons or pinch to zoom", + style: TextStyle( + color: Colors.white, + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], ); } diff --git a/lib/widgets/location.dart b/lib/widgets/location.dart index e31304f..333d9c3 100644 --- a/lib/widgets/location.dart +++ b/lib/widgets/location.dart @@ -71,7 +71,6 @@ class _LocationWidgetState extends State { Location location = Location(); try { - // Check if location service is enabled bool serviceEnabled = await location.serviceEnabled(); if (!serviceEnabled) { serviceEnabled = await location.requestService(); @@ -83,8 +82,6 @@ class _LocationWidgetState extends State { return; } } - - // Check and request location permissions PermissionStatus permissionGranted = await location.hasPermission(); if (permissionGranted == PermissionStatus.denied) { permissionGranted = await location.requestPermission(); @@ -96,8 +93,6 @@ class _LocationWidgetState extends State { return; } } - - // Get current location LocationData locationData = await location.getLocation(); setState(() { @@ -115,7 +110,6 @@ class _LocationWidgetState extends State { } } -// Example usage in your main app class LocationApp extends StatelessWidget { const LocationApp({Key? key}) : super(key: key); diff --git a/lib/widgets/tree_nft_view_details_with_map.dart b/lib/widgets/tree_nft_view_details_with_map.dart new file mode 100644 index 0000000..26a927b --- /dev/null +++ b/lib/widgets/tree_nft_view_details_with_map.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/services/wallet_provider_utils.dart'; +import 'package:tree_planting_protocol/widgets/flutter_map_widget.dart'; + +class NewNFTMapWidget extends StatefulWidget { + const NewNFTMapWidget({super.key}); + + @override + State createState() => _NewNFTMapWidgetState(); +} + +class _NewNFTMapWidgetState extends State { + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final screenWidth = screenSize.width; + final screenHeight = screenSize.height; + + // Responsive dimensions + final mapHeight = screenHeight * 0.35; + final mapWidth = screenWidth * 0.9; + final containerMaxWidth = screenWidth * 0.95; + + return SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: screenWidth * 0.05, + vertical: 16.0, + ), + child: Column( + children: [ + Container( + height: mapHeight.clamp(250.0, 400.0), + width: mapWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: StaticDisplayMap( + lat: Provider.of(context).getLatitude().toDouble(), + lng: Provider.of(context).getLongitude().toDouble(), + ), + ), + + const SizedBox(height: 20), + + // Information Card + Container( + width: double.infinity, + constraints: BoxConstraints( + maxWidth: containerMaxWidth, + minHeight: 120, + ), + padding: EdgeInsets.all(screenWidth * 0.04), // Responsive padding + decoration: BoxDecoration( + border: Border.all(color: Colors.green, width: 2), + borderRadius: BorderRadius.circular(12.0), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Consumer( + builder: (ctx, provider, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow( + 'Latitude:', + provider.getLatitude().toString(), + screenWidth, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Longitude:', + provider.getLongitude().toString(), + screenWidth, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'GeoHash:', + provider.getGeoHash(), + screenWidth, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Species:', + provider.getSpecies(), + screenWidth, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Description:', + _formatDescription(provider.getDescription(), screenWidth), + screenWidth, + isDescription: true, + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: provider.getInitialPhotos().length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Image.network( + provider.getInitialPhotos()[index], + height: 100, + width: double.infinity, + fit: BoxFit.cover, + ), + ); + } + ), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value, double screenWidth, {bool isDescription = false}) { + final fontSize = screenWidth < 360 ? 14.0 : 16.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Text( + value, + style: TextStyle( + fontSize: fontSize, + color: Colors.grey.shade800, + height: isDescription ? 1.4 : 1.2, + ), + softWrap: true, + ), + ), + ], + ); + } +} + +String _formatDescription(String description, double screenWidth) { + int maxLength; + if (screenWidth < 360) { + maxLength = 60; + } else if (screenWidth < 600) { + maxLength = 100; + } else { + maxLength = 150; + } + + return description.length > maxLength + ? '${description.substring(0, maxLength)}...' + : description; +} \ No newline at end of file From 15713d5843a12b53479671c7a063b3c7917fa655 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Thu, 31 Jul 2025 15:19:59 +0530 Subject: [PATCH 17/18] feat: minting tree nft --- lib/components/universal_navbar.dart | 2 +- lib/main.dart | 8 - lib/pages/counter_page.dart | 510 ------------------ lib/pages/home_page.dart | 5 - lib/pages/mint_nft/submit_nft_page.dart | 194 ++++++- lib/providers/wallet_provider.dart | 22 +- lib/utils/constants/bottom_nav_constants.dart | 7 - lib/utils/constants/contractDetails.dart | 6 + .../contract_abis/tree_nft_contract_abi.dart | 4 + lib/widgets/location.dart | 135 ----- 10 files changed, 213 insertions(+), 680 deletions(-) delete mode 100644 lib/pages/counter_page.dart create mode 100644 lib/utils/constants/contractDetails.dart create mode 100644 lib/utils/constants/contract_abis/tree_nft_contract_abi.dart delete mode 100644 lib/widgets/location.dart diff --git a/lib/components/universal_navbar.dart b/lib/components/universal_navbar.dart index 72062d8..3084479 100644 --- a/lib/components/universal_navbar.dart +++ b/lib/components/universal_navbar.dart @@ -337,7 +337,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { const PopupMenuItem( value: 'Switch Chain', child: ListTile( - leading: Icon(Icons.logout, color: Colors.red, size: 20), + leading: Icon(Icons.switch_access_shortcut, color: Colors.green, size: 20), title: Text( 'Switch Chain', style: TextStyle(color: Colors.green), diff --git a/lib/main.dart b/lib/main.dart index d2180c6..94aeefd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,6 @@ import 'package:tree_planting_protocol/pages/mint_nft/submit_nft_page.dart'; import 'package:tree_planting_protocol/pages/settings_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; -import 'package:tree_planting_protocol/pages/counter_page.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/providers/theme_provider.dart'; @@ -56,13 +55,6 @@ class MyApp extends StatelessWidget { return const SettingsPage(); }, ), - GoRoute( - path: '/counter', - name: 'counter_page', - builder: (BuildContext context, GoRouterState state) { - return CounterPage(); - }, - ), GoRoute( path: RouteConstants.mintNftPath, name: RouteConstants.mintNft, diff --git a/lib/pages/counter_page.dart b/lib/pages/counter_page.dart deleted file mode 100644 index 15e0cee..0000000 --- a/lib/pages/counter_page.dart +++ /dev/null @@ -1,510 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:tree_planting_protocol/providers/wallet_provider.dart'; -import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; - -class CounterPage extends StatefulWidget { - const CounterPage({super.key}); - - @override - State createState() => _CounterPageState(); -} - -class _CounterPageState extends State { - static final String contractAddress = dotenv.env['CONTRACT_ADDRESS'] ?? '0xa122109493B90e322824c3444ed8D6236CAbAB7C'; - static const String chainId = '11155111'; - - static const List> contractAbi = [ - { - "inputs": [], - "name": "getCount", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "increment", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]; - - String? currentCount; - bool isLoading = false; - bool isIncrementing = false; - String? errorMessage; - String? lastTransactionHash; - - @override - void initState() { - super.initState(); - _loadCount(); - } - - Future _loadCount() async { - setState(() { - isLoading = true; - errorMessage = null; - }); - - try { - final walletProvider = Provider.of(context, listen: false); - - final result = await walletProvider.readContract( - contractAddress: contractAddress, - functionName: 'getCount', - abi: contractAbi, - ); - - setState(() { - currentCount = result.isNotEmpty ? result[0].toString() : '0'; - isLoading = false; - }); - } catch (e) { - setState(() { - errorMessage = e.toString(); - isLoading = false; - }); - } - } - - Future _incrementCount() async { - final walletProvider = Provider.of(context, listen: false); - - if (!walletProvider.isConnected) { - _showErrorDialog('Wallet Not Connected', - 'Please connect your wallet to increment the counter.'); - return; - } - - if (walletProvider.currentChainId != chainId) { - _showErrorDialog('Wrong Network', - 'Please switch to Sepolia testnet (Chain ID: $chainId) to interact with this contract.'); - return; - } - - setState(() { - isIncrementing = true; - }); - - try { - final txHash = await walletProvider.writeContract( - contractAddress: contractAddress, - functionName: 'increment', - abi: contractAbi, - chainId: chainId, - ); - - setState(() { - lastTransactionHash = txHash; - isIncrementing = false; - }); - - _showSuccessDialog('Transaction Sent!', - 'Transaction hash: ${txHash.substring(0, 10)}...\n\nThe counter will update once the transaction is confirmed.'); - Future.delayed(const Duration(seconds: 3), () { - if (mounted) { - _loadCount(); - } - }); - - } catch (e) { - setState(() { - isIncrementing = false; - }); - _showErrorDialog('Transaction Failed', e.toString()); - } - } - - void _showErrorDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(title), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, - ); - } - - void _showSuccessDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - const Icon(Icons.check_circle, color: Colors.green), - const SizedBox(width: 8), - Text(title), - ], - ), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return BaseScaffold( - title: 'Counter', - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Contract Info Card - Card( - elevation: 4, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Contract Information', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - _buildInfoRow('Address:', contractAddress), - _buildInfoRow('Chain ID:', chainId), - _buildInfoRow('Network:', 'Sepolia Testnet'), - _buildInfoRow('Function:', 'getCount()'), - ], - ), - ), - ), - - const SizedBox(height: 24), - Card( - elevation: 4, - color: Theme.of(context).primaryColor.withOpacity(0.1), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - children: [ - const Text( - 'Current Count', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 16), - if (isLoading) - const Column( - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Loading count...'), - ], - ) - else if (errorMessage != null) - Column( - children: [ - Icon( - Icons.error_outline, - size: 48, - color: Colors.red[400], - ), - const SizedBox(height: 8), - Text( - 'Error: $errorMessage', - style: TextStyle( - color: Colors.red[600], - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - ], - ) - else - Column( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, - ), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).primaryColor, - width: 2, - ), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - currentCount ?? '0', - style: const TextStyle( - fontSize: 48, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 8), - Text( - 'Last updated: ${DateTime.now().toString().substring(11, 19)}', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), - ], - ), - ], - ), - ), - ), - - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: isLoading ? null : _loadCount, - icon: const Icon(Icons.refresh), - label: const Text('Refresh Count'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - textStyle: const TextStyle(fontSize: 16), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Consumer( - builder: (context, walletProvider, child) { - final bool canIncrement = walletProvider.isConnected && - walletProvider.currentChainId == chainId && - !isIncrementing && !isLoading; - - return ElevatedButton.icon( - onPressed: canIncrement ? _incrementCount : null, - icon: isIncrementing - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.add), - label: Text(isIncrementing ? 'Incrementing...' : 'Increment'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - textStyle: const TextStyle(fontSize: 16), - ), - ); - }, - ), - ), - ], - ), - if (lastTransactionHash != null) ...[ - const SizedBox(height: 16), - Card( - color: Colors.green[50], - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.check_circle, color: Colors.green, size: 16), - SizedBox(width: 8), - Text( - 'Last Transaction', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Hash: ${lastTransactionHash!.substring(0, 20)}...', - style: const TextStyle( - fontSize: 12, - fontFamily: 'monospace', - ), - ), - const SizedBox(height: 4), - Text( - 'View on Etherscan', - style: TextStyle( - fontSize: 12, - color: Colors.blue[600], - decoration: TextDecoration.underline, - ), - ), - ], - ), - ), - ), - ], - - const Spacer(), - Consumer( - builder: (context, walletProvider, child) { - return Card( - color: Colors.grey[50], - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Connection Status', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - walletProvider.isConnected - ? Icons.check_circle - : Icons.error_outline, - size: 16, - color: walletProvider.isConnected - ? Colors.green - : Colors.orange, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - walletProvider.isConnected - ? 'Wallet Connected' - : 'Wallet Not Connected (Read-only mode)', - style: const TextStyle(fontSize: 12), - ), - ), - ], - ), - if (walletProvider.isConnected) ...[ - if (walletProvider.currentChainId != null) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - children: [ - Icon( - walletProvider.currentChainId == chainId - ? Icons.check_circle - : Icons.warning, - size: 14, - color: walletProvider.currentChainId == chainId - ? Colors.green - : Colors.orange, - ), - const SizedBox(width: 6), - Text( - 'Chain: ${walletProvider.currentChainId} ${walletProvider.currentChainId == chainId ? '(Correct)' : '(Switch to $chainId)'}', - style: TextStyle( - fontSize: 12, - color: walletProvider.currentChainId == chainId - ? Colors.green[700] - : Colors.orange[700], - ), - ), - ], - ), - ), - if (walletProvider.currentAddress != null) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - 'Address: ${walletProvider.currentAddress!.substring(0, 6)}...${walletProvider.currentAddress!.substring(walletProvider.currentAddress!.length - 4)}', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - fontFamily: 'monospace', - ), - ), - ), - ] else ...[ - const Padding( - padding: EdgeInsets.only(top: 4), - child: Text( - 'Connect wallet to increment counter', - style: TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ], - ), - ), - ); - }, - ), - ], - ), - ), - ); - } - - Widget _buildInfoRow(String label, String value) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 80, - child: Text( - label, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - ), - ), - Expanded( - child: Text( - value, - style: const TextStyle( - fontSize: 14, - fontFamily: 'monospace', - ), - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 8cb49f6..e1fc79e 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,7 +3,6 @@ import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/utils/constants/navbar_constants.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; -import 'package:tree_planting_protocol/widgets/location.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -24,10 +23,6 @@ class HomePage extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 32), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300, maxHeight: 300), - child: const LocationWidget(), - ), const SizedBox(height: 16), ElevatedButton( onPressed: () { diff --git a/lib/pages/mint_nft/submit_nft_page.dart b/lib/pages/mint_nft/submit_nft_page.dart index 9d1b2eb..0e385a6 100644 --- a/lib/pages/mint_nft/submit_nft_page.dart +++ b/lib/pages/mint_nft/submit_nft_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/tree_nft_view_details_with_map.dart'; - - +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/constants/contract_abis/tree_nft_contract_abi.dart'; class SubmitNFTPage extends StatefulWidget { const SubmitNFTPage({super.key}); @@ -14,11 +16,193 @@ class SubmitNFTPage extends StatefulWidget { } class _SubmitNFTPageState extends State { + static final String contractAddress = dotenv.env['CONTRACT_ADDRESS'] ?? + '0xa122109493B90e322824c3444ed8D6236CAbAB7C'; + + bool isLoading = false; + bool isMinting = false; + String? errorMessage; + String? lastTransactionHash; + + void _showSuccessDialog(String title, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.green), + const SizedBox(width: 8), + Text(title), + ], + ), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + void _showErrorDialog(String title, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + Future _mintTreeNft() async { + final walletProvider = Provider.of(context, listen: false); + final mintNftProvider = + Provider.of(context, listen: false); + + if (!walletProvider.isConnected) { + logger.e("Please connect your wallet before putting this request"); + _showErrorDialog( + 'Wallet Not Connected', 'Please connect your wallet before minting.'); + return; + } + + setState(() { + isMinting = true; + errorMessage = null; + }); + + try { + final double rawLat = mintNftProvider.getLatitude(); + final double rawLng = mintNftProvider.getLongitude(); + + if (rawLat < -90.0 || + rawLat > 90.0 || + rawLng < -180.0 || + rawLng > 180.0) { + _showErrorDialog( + 'Invalid Coordinates', + 'The selected coordinates are outside the valid range.\nLat: [-90, 90], Lng: [-180, 180].', + ); + return; + } + final lat = BigInt.from((mintNftProvider.getLatitude() + 90.0) * 1e6); + final lng = BigInt.from((mintNftProvider.getLongitude() + 180.0) * 1e6); + logger.i("Calculated values being sent: Lat: $lat, Lng: $lng"); + List args = [ + lat, + lng, + mintNftProvider.getSpecies(), + "sampleHash", + "sameQRIPFSHash", + mintNftProvider.getGeoHash(), + mintNftProvider.getInitialPhotos(), + ]; + + final txHash = await walletProvider.writeContract( + contractAddress: TreeNFtContractAddress, + functionName: 'mintNft', + params: args, + abi: TreeNftContractABI, + chainId: walletProvider.currentChainId, + ); + + setState(() { + lastTransactionHash = txHash; + isMinting = false; + }); + + _showSuccessDialog( + 'Transaction Sent!', + 'Transaction hash: ${txHash.substring(0, 10)}...\n\nThe NFT will be minted once the transaction is confirmed.', + ); + } catch (e) { + logger.e("Error occurred", error: e); + setState(() { + isMinting = false; + errorMessage = e.toString(); + }); + _showErrorDialog('Transaction Failed', e.toString()); + } + } + @override Widget build(BuildContext context) { - return const BaseScaffold( + return BaseScaffold( title: "Submit NFT", - body: NewNFTMapWidget() + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const NewNFTMapWidget(), + const SizedBox(height: 30), + if (errorMessage != null) + Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200), + ), + child: Text( + errorMessage!, + style: TextStyle(color: Colors.red.shade700), + ), + ), + ElevatedButton( + onPressed: isMinting ? null : _mintTreeNft, + child: isMinting + ? const Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 8), + Text("Minting..."), + ], + ) + : const Text("Mint NFT"), + ), + if (lastTransactionHash != null) + Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + children: [ + const Text( + "Last Transaction:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + lastTransactionHash!.substring(0, 20) + "...", + style: const TextStyle(fontFamily: 'monospace'), + ), + ], + ), + ), + ], + ), + ), ); } -} \ No newline at end of file +} diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 917ce15..069852c 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -436,7 +436,7 @@ class WalletProvider extends ChangeNotifier { Future writeContract({ required String contractAddress, required String functionName, - required dynamic abi, + required String abi, // Changed to only accept String String? chainId, List params = const [], BigInt? value, @@ -446,34 +446,36 @@ class WalletProvider extends ChangeNotifier { if (!_isConnected || _web3App == null || _currentAddress == null) { throw Exception('Wallet not connected'); } + _updateStatus('Preparing transaction...'); - List abiList; - if (abi is String) { - abiList = json.decode(abi); - } else if (abi is List) { - abiList = abi; - } else { - throw Exception('Invalid ABI format'); - } + + // Decode the JSON ABI string + final abiList = json.decode(abi) as List; + final contract = DeployedContract( ContractAbi.fromJson(json.encode(abiList), ''), EthereumAddress.fromHex(contractAddress), ); + final function = contract.function(functionName); final encodedFunction = function.encodeCall(params); final targetChainId = chainId ?? _currentChainId ?? _defaultChainId; + if (_currentChainId != targetChainId) { logger.w( 'Target chain ($targetChainId) differs from current chain ($_currentChainId)'); _updateStatus( 'Chain mismatch detected. Current: $_currentChainId, Target: $targetChainId'); } + final rpcUrl = getChainDetails(targetChainId).first['rpcUrl'] as String?; final httpClient = http.Client(); final ethClient = Web3Client(rpcUrl as String, httpClient); + final nonce = await ethClient.getTransactionCount( EthereumAddress.fromHex(_currentAddress!), ); + BigInt estimatedGas = gasLimit ?? BigInt.from(100000); if (gasLimit == null) { try { @@ -488,8 +490,10 @@ class WalletProvider extends ChangeNotifier { logger.w('Gas estimation failed, using default: $e'); } } + final gasPrice = await ethClient.getGasPrice(); httpClient.close(); + final transaction = { 'from': _currentAddress!, 'to': contractAddress, diff --git a/lib/utils/constants/bottom_nav_constants.dart b/lib/utils/constants/bottom_nav_constants.dart index f1915b9..2d75f8d 100644 --- a/lib/utils/constants/bottom_nav_constants.dart +++ b/lib/utils/constants/bottom_nav_constants.dart @@ -29,13 +29,6 @@ class BottomNavConstants { activeIcon: Icons.forest, route: RouteConstants.allTreesPath, ), - BottomNavItem( - label: 'Counter', - icon: Icons.nature_people_outlined, - activeIcon: Icons.nature_people, - route: '/counter', - ), - BottomNavItem( label: 'Settings', icon: Icons.settings_outlined, diff --git a/lib/utils/constants/contractDetails.dart b/lib/utils/constants/contractDetails.dart new file mode 100644 index 0000000..463fcfd --- /dev/null +++ b/lib/utils/constants/contractDetails.dart @@ -0,0 +1,6 @@ +// CareToken Address: 0x7F1461435b702ad583C61Da69A186eC0Dfe5fce2 +// PlanterToken Address: 0x2F735217076186108dfef7270C007a99A7C83df2 +// VerifierToken Address: 0x2b26a0b0523C29D1f1D41411982e81cd99f8b0f3 +// LegacyToken Address: 0x30c7f2930f82672A54a831B301d097a6Bde1D5cC +// TreeNft Address: 0x757d34D7876BB111DA3646d0b413F3cE9cA39484 +// OrganisationFactory: 0xCcAEb57DEFE17E9dc3CB2A47EE054E02aD51E635 \ No newline at end of file diff --git a/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart b/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart new file mode 100644 index 0000000..e4c30a7 --- /dev/null +++ b/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart @@ -0,0 +1,4 @@ +const String TreeNftContractABI = ''' [{"type":"constructor","inputs":[{"name":"_careTokenContract","type":"address","internalType":"address"},{"name":"_planterTokenContract","type":"address","internalType":"address"},{"name":"_verifierTokenContract","type":"address","internalType":"address"},{"name":"_legacyTokenContract","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"approve","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"balanceOf","inputs":[{"name":"owner","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"careTokenContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract CareToken"}],"stateMutability":"view"},{"type":"function","name":"getAllNFTs","inputs":[],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getApproved","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getNFTsByUser","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getNFTsByUserPaginated","inputs":[{"name":"user","type":"address","internalType":"address"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"trees","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRecentTreesPaginated","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"paginatedTrees","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"},{"name":"hasMore","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getTreeDetailsbyID","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct Tree","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getTreeNftVerifiers","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct TreeNftVerification[]","components":[{"name":"verifier","type":"address","internalType":"address"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"description","type":"string","internalType":"string"},{"name":"isHidden","type":"bool","internalType":"bool"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getTreeNftVerifiersPaginated","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"verifications","type":"tuple[]","internalType":"struct TreeNftVerification[]","components":[{"name":"verifier","type":"address","internalType":"address"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"description","type":"string","internalType":"string"},{"name":"isHidden","type":"bool","internalType":"bool"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"},{"name":"visiblecount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getVerifiedTreesByUser","inputs":[{"name":"verifier","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getVerifiedTreesByUserPaginated","inputs":[{"name":"verifier","type":"address","internalType":"address"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"trees","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"isApprovedForAll","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"operator","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isVerified","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"verifier","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"legacyToken","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract LegacyToken"}],"stateMutability":"view"},{"type":"function","name":"markDead","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"minimumTimeToMarkTreeDead","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"mintNft","inputs":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"initialPhotos","type":"string[]","internalType":"string[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"ownerOf","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"planterTokenContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract PlanterToken"}],"stateMutability":"view"},{"type":"function","name":"registerUserProfile","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_profilePhotoHash","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeVerification","inputs":[{"name":"_verificationId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"safeTransferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"safeTransferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setApprovalForAll","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"approved","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"tokenURI","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateUserDetails","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_profilePhotoHash","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"verifierTokenContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract VerifierToken"}],"stateMutability":"view"},{"type":"function","name":"verify","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"},{"name":"_proofHashes","type":"string[]","internalType":"string[]"},{"name":"_description","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ApprovalForAll","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"operator","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Transfer","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VerificationRemoved","inputs":[{"name":"verificationId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"treeNftId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"verifier","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"ERC721IncorrectOwner","inputs":[{"name":"sender","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InsufficientApproval","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ERC721InvalidApprover","inputs":[{"name":"approver","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidOperator","inputs":[{"name":"operator","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidReceiver","inputs":[{"name":"receiver","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidSender","inputs":[{"name":"sender","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721NonexistentToken","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidCoordinates","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidTreeID","inputs":[]},{"type":"error","name":"MinimumMarkDeadTimeNotReached","inputs":[]},{"type":"error","name":"NotTreeOwner","inputs":[]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]},{"type":"error","name":"PaginationLimitExceeded","inputs":[]},{"type":"error","name":"TreeAlreadyDead","inputs":[]},{"type":"error","name":"UserAlreadyRegistered","inputs":[]},{"type":"error","name":"UserNotRegistered","inputs":[]}]'''; + +const String TreeNFtContractAddress = + "0x6d19e13a9a0A43267467E35B5393439562fD066f"; diff --git a/lib/widgets/location.dart b/lib/widgets/location.dart deleted file mode 100644 index 333d9c3..0000000 --- a/lib/widgets/location.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:location/location.dart'; - -class LocationWidget extends StatefulWidget { - const LocationWidget({Key? key}) : super(key: key); - - @override - State createState() => _LocationWidgetState(); -} - -class _LocationWidgetState extends State { - String _locationMessage = "Location not determined yet"; - bool _isLoading = false; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16.0), - margin: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue.shade200), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.location_on, - size: 48, - color: Colors.blue, - ), - const SizedBox(height: 16), - Text( - 'Current Location', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.blue.shade800, - ), - ), - const SizedBox(height: 16), - _isLoading - ? const CircularProgressIndicator() - : Text( - _locationMessage, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _isLoading ? null : _getCurrentLocation, - icon: const Icon(Icons.my_location), - label: const Text('Get Location'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), - ), - ], - ), - ); - } - - Future _getCurrentLocation() async { - setState(() { - _isLoading = true; - _locationMessage = "Getting location..."; - }); - - Location location = Location(); - - try { - bool serviceEnabled = await location.serviceEnabled(); - if (!serviceEnabled) { - serviceEnabled = await location.requestService(); - if (!serviceEnabled) { - setState(() { - _locationMessage = "Location services are disabled. Please enable them."; - _isLoading = false; - }); - return; - } - } - PermissionStatus permissionGranted = await location.hasPermission(); - if (permissionGranted == PermissionStatus.denied) { - permissionGranted = await location.requestPermission(); - if (permissionGranted != PermissionStatus.granted) { - setState(() { - _locationMessage = "Location permissions are denied."; - _isLoading = false; - }); - return; - } - } - LocationData locationData = await location.getLocation(); - - setState(() { - _locationMessage = "Latitude: ${locationData.latitude?.toStringAsFixed(6) ?? 'N/A'}\n" - "Longitude: ${locationData.longitude?.toStringAsFixed(6) ?? 'N/A'}\n" - "Accuracy: ${locationData.accuracy?.toStringAsFixed(2) ?? 'N/A'}m"; - _isLoading = false; - }); - } catch (e) { - setState(() { - _locationMessage = "Error getting location: $e"; - _isLoading = false; - }); - } - } -} - -class LocationApp extends StatelessWidget { - const LocationApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Location Widget Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: Scaffold( - appBar: AppBar( - title: const Text('Location Widget'), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - body: const Center( - child: LocationWidget(), - ), - ), - ); - } -} \ No newline at end of file From af3e39f07a1603c14a2525cea8218d2b855d843a Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Fri, 1 Aug 2025 09:54:56 +0530 Subject: [PATCH 18/18] refactor: logging --- lib/main.dart | 2 -- lib/pages/mint_nft/mint_nft_coordinates.dart | 16 +--------------- lib/widgets/flutter_map_widget.dart | 16 +++++----------- lib/widgets/tree_NFT_view_widget.dart | 1 - lib/widgets/tree_nft_view_details_with_map.dart | 7 +------ 5 files changed, 7 insertions(+), 35 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 94aeefd..9e5d9f1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,8 +21,6 @@ import 'package:tree_planting_protocol/utils/logger.dart'; class NavigationService { static GlobalKey navigatorKey = GlobalKey(); } - -final contractAddress = "0xa122109493B90e322824c3444ed8D6236CAbAB7C"; void main() async { try { await dotenv.load(fileName: ".env"); diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index 709bd01..eeb6705 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -219,13 +219,11 @@ class _MintNftCoordinatesPageState extends State { ), child: Column( children: [ - // Form Section + _buildFormSection(screenWidth, screenHeight), const SizedBox(height: 32), - // Preview Section - _buildPreviewSection(), ], ), ), @@ -257,7 +255,6 @@ class _MintNftCoordinatesPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Form Header Container( width: double.infinity, padding: const EdgeInsets.all(24), @@ -319,17 +316,10 @@ class _MintNftCoordinatesPageState extends State { padding: const EdgeInsets.all(24), child: Column( children: [ - // Location Status _buildLocationStatus(), - const SizedBox(height: 24), - - // Map Section _buildMapSection(screenHeight), - const SizedBox(height: 24), - - // Instructions Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -362,8 +352,6 @@ class _MintNftCoordinatesPageState extends State { ), const SizedBox(height: 24), - - // Coordinate Fields Row( children: [ Expanded( @@ -387,8 +375,6 @@ class _MintNftCoordinatesPageState extends State { ), const SizedBox(height: 32), - - // Submit Button SizedBox( width: double.infinity, height: 56, diff --git a/lib/widgets/flutter_map_widget.dart b/lib/widgets/flutter_map_widget.dart index 8078d90..0d2ffe3 100644 --- a/lib/widgets/flutter_map_widget.dart +++ b/lib/widgets/flutter_map_widget.dart @@ -3,6 +3,7 @@ import 'package:latlong2/latlong.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; class CoordinatesMap extends StatefulWidget { final Function(double lat, double lng)? onLocationSelected; @@ -18,9 +19,7 @@ class _CoordinatesMapState extends State { bool _mapLoaded = false; bool _hasError = false; String? _errorMessage; - - // Default location (you can change this to any default location) - static const double _defaultLat = 28.9845; // Example: Roorkee, India + static const double _defaultLat = 28.9845; static const double _defaultLng = 77.8956; @override @@ -103,7 +102,7 @@ class _CoordinatesMapState extends State { mapController: _mapController, options: MapOptions( initialCenter: LatLng(latitude, longitude), - initialZoom: 15.0, + initialZoom: 1.0, minZoom: 3.0, maxZoom: 18.0, interactionOptions: const InteractionOptions( @@ -325,7 +324,7 @@ class _StaticDisplayMapState extends State { } double _sanitizeCoordinate(double value, double defaultValue) { if (value.isNaN || value.isInfinite || value == double.infinity || value == double.negativeInfinity) { - print('Invalid coordinate detected: $value, using default: $defaultValue'); + logger.e('Invalid coordinate detected: $value, using default: $defaultValue'); return defaultValue; } return value; @@ -338,10 +337,6 @@ class _StaticDisplayMapState extends State { latitude = latitude.clamp(-90.0, 90.0); longitude = longitude.clamp(-180.0, 180.0); - // Debug print to see what values we're getting - print('StaticDisplayMap - lat: $latitude, lng: $longitude'); - print('Widget.lat: ${widget.lat}, Widget.lng: ${widget.lng}'); - return Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), @@ -401,9 +396,8 @@ class _StaticDisplayMapState extends State { } Widget _buildMapWidget(double latitude, double longitude) { - // Final safety check before creating LatLng if (latitude.isNaN || latitude.isInfinite || longitude.isNaN || longitude.isInfinite) { - print('ERROR: Invalid coordinates in _buildMapWidget - lat: $latitude, lng: $longitude'); + logger.e('ERROR: Invalid coordinates in _buildMapWidget - lat: $latitude, lng: $longitude'); latitude = _defaultLat; longitude = _defaultLng; } diff --git a/lib/widgets/tree_NFT_view_widget.dart b/lib/widgets/tree_NFT_view_widget.dart index 3747be0..0567264 100644 --- a/lib/widgets/tree_NFT_view_widget.dart +++ b/lib/widgets/tree_NFT_view_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; -import 'package:tree_planting_protocol/utils/services/wallet_provider_utils.dart'; class NewNFTWidget extends StatefulWidget { const NewNFTWidget({super.key}); diff --git a/lib/widgets/tree_nft_view_details_with_map.dart b/lib/widgets/tree_nft_view_details_with_map.dart index 26a927b..ab10d51 100644 --- a/lib/widgets/tree_nft_view_details_with_map.dart +++ b/lib/widgets/tree_nft_view_details_with_map.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; -import 'package:tree_planting_protocol/utils/services/wallet_provider_utils.dart'; import 'package:tree_planting_protocol/widgets/flutter_map_widget.dart'; class NewNFTMapWidget extends StatefulWidget { @@ -17,8 +16,6 @@ class _NewNFTMapWidgetState extends State { final screenSize = MediaQuery.of(context).size; final screenWidth = screenSize.width; final screenHeight = screenSize.height; - - // Responsive dimensions final mapHeight = screenHeight * 0.35; final mapWidth = screenWidth * 0.9; final containerMaxWidth = screenWidth * 0.95; @@ -51,15 +48,13 @@ class _NewNFTMapWidgetState extends State { ), const SizedBox(height: 20), - - // Information Card Container( width: double.infinity, constraints: BoxConstraints( maxWidth: containerMaxWidth, minHeight: 120, ), - padding: EdgeInsets.all(screenWidth * 0.04), // Responsive padding + padding: EdgeInsets.all(screenWidth * 0.04), decoration: BoxDecoration( border: Border.all(color: Colors.green, width: 2), borderRadius: BorderRadius.circular(12.0),