diff --git a/app/lib/providers/farms_provider.dart b/app/lib/providers/farms_provider.dart new file mode 100644 index 00000000..211440da --- /dev/null +++ b/app/lib/providers/farms_provider.dart @@ -0,0 +1,323 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mutex/mutex.dart'; +import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/models/farm.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/crypto_service.dart'; +import 'package:threebotlogin/services/gridproxy_service.dart'; +import 'package:threebotlogin/services/tfchain_service.dart' as TFChainService; +import 'package:registrar_client/registrar_client.dart' as registrar; +import 'package:registrar_client/models/farm.dart' as registrarFarm; +import 'package:registrar_client/models/node.dart' as registrarNode; + +class FarmsNotifier extends StateNotifier> { + FarmsNotifier() : super([]); + + bool _loading = true; + bool _isListed = false; + bool _preloading = false; + final Mutex _mutex = Mutex(); + final Map _farmCache = {}; + final Duration _cacheTimeout = const Duration(minutes: 5); + + List _v3Farms = []; + List _v4Farms = []; + bool _reload = false; + + bool get isLoading => _loading; + bool get isListed => _isListed; + bool get isPreloading => _preloading; + + Future list(List wallets) async { + if (_isListed && state.isNotEmpty) return; + _loading = true; + try { + await _mutex.protect(() async { + final farms = await _listFarms(wallets); + state = farms; + _isListed = true; + }); + } catch (e) { + logger.e('Failed to load farms: $e'); + if (state.isEmpty) { + rethrow; + } + } finally { + _loading = false; + } + } + + Future refresh(List wallets) async { + _loading = true; + _isListed = false; + _farmCache.clear(); + + await _mutex.protect(() async { + state = await _listFarms(wallets); + }); + _loading = false; + _isListed = true; + } + + /// Preload farm data in background for faster page transitions + Future preloadFarmData(List wallets) async { + if (_preloading || _isListed) return; + _preloading = true; + + try { + logger.i('Preloading farm data in background...'); + + // Load farms without blocking UI + final farms = await _listFarms(wallets); + if (farms.isNotEmpty && !_isListed) { + await _mutex.protect(() async { + state = farms; + _isListed = true; + }); + } + logger.i('Farm data preloaded successfully'); + } catch (e) { + logger.e('Failed to preload farm data: $e'); + } finally { + _preloading = false; + } + } + + /// Internal method to load farms from wallets + Future> _listFarms(List wallets) async { + if (wallets.isEmpty) return []; + + final now = DateTime.now(); + final cacheKey = 'farms_${wallets.map((w) => w.name).join('_')}'; + + // Check cache first - if we have cached data and it's still valid, return it + if (_farmCache.containsKey(cacheKey) && state.isNotEmpty) { + final cacheTime = _farmCache[cacheKey]!; + if (now.difference(cacheTime) < _cacheTimeout) { + logger.i('Using cached farm data'); + return state; + } + } + final twinIdFutures = wallets.map((wallet) async { + try { + final twinId = await TFChainService.getTwinId(wallet.tfchainSecret); + return twinId != 0 ? MapEntry(twinId, wallet) : null; + } catch (e) { + logger.e('Failed to get twin ID for ${wallet.name}: $e'); + return null; + } + }).toList(); + + final twinIdResults = await Future.wait(twinIdFutures); + final Map twinIdWallets = {}; + + for (final result in twinIdResults) { + if (result != null) { + twinIdWallets[result.key] = result.value; + } + } + + if (twinIdWallets.isEmpty) { + return []; + } + + try { + final v3FarmsFuture = _loadV3Farms(twinIdWallets); + final v4FarmsFuture = _loadV4Farms(twinIdWallets); + + final results = await Future.wait([v3FarmsFuture, v4FarmsFuture]); + final v3Farms = results[0]; + final v4Farms = results[1]; + + _v3Farms = v3Farms; + _v4Farms = v4Farms; + + final allFarms = [...v3Farms, ...v4Farms]; + + _farmCache[cacheKey] = now; + + return allFarms; + } catch (e) { + logger.e('Failed to load farms from network: $e'); + + if (state.isNotEmpty) { + logger.i('Returning cached farm data due to network error'); + return state; + } + + rethrow; + } + } + + Future> _loadV3Farms(Map twinIdWallets) async { + try { + final farmsList = await getFarmsByTwinIds(twinIdWallets.keys.toList()); + + final farmFutures = farmsList.map((farm) async { + try { + final nodes = await getNodesByFarmId(farm.farmID); + final wallet = twinIdWallets[farm.twinId]!; + + return Farm( + name: farm.name, + walletAddress: farm.stellarAddress, + tfchainWalletSecret: wallet.tfchainSecret, + walletName: wallet.name, + twinId: farm.twinId, + farmId: farm.farmID, + nodes: nodes.map((node) => Node( + nodeId: node.nodeId, + status: NodeStatus.values.firstWhere( + (e) => e.toString().toLowerCase() == 'nodestatus.${node.status}', + ), + country: node.location!.country, + uptime: node.uptime, + )).toList(), + ); + } catch (e) { + logger.e('Failed to load V3 farm ${farm.name}: $e'); + return null; + } + }).toList(); + + final results = await Future.wait(farmFutures); + return results.where((farm) => farm != null).cast().toList(); + } catch (e) { + logger.e('Failed to load V3 farms: $e'); + return []; + } + } + + Future> _loadV4Farms(Map twinIdWallets) async { + try { + final List allV4Farms = []; + + for (final entry in twinIdWallets.entries) { + final wallet = entry.value; + + try { + final registrarClient = registrar.RegistrarClient( + baseUrl: Globals().registrarURL, + mnemonicOrSeed: wallet.tfchainSecret, + ); + + final publicKey = await derivePublicKey(wallet.tfchainSecret); + final account = await registrarClient.accounts.getByPublicKey(publicKey); + final v4Farms = await registrarClient.farms.list( + registrarFarm.FarmFilter(twinID: account.twinID), + ); + for (final v4Farm in v4Farms) { + try { + final farmNodes = await registrarClient.nodes.list( + registrarNode.NodeFilter(farmID: v4Farm.farmID), + ); + + final nodes = farmNodes.map((n) { + return Node( + nodeId: n.nodeID, + status: n.online ? NodeStatus.Up : NodeStatus.Down, + country: n.location.country, + uptime: (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0, + ); + }).toList(); + + final farm = Farm( + name: 'Farm ${v4Farm.farmID}', + walletAddress: wallet.stellarAddress, + tfchainWalletSecret: wallet.tfchainSecret, + walletName: wallet.name, + twinId: v4Farm.twinID, + farmId: v4Farm.farmID!, + nodes: nodes, + ); + + allV4Farms.add(farm); + } catch (e) { + logger.e('Failed to load V4 farm ${v4Farm.farmID}: $e'); + continue; + } + } + } catch (e) { + logger.e('Failed to load V4 farms for wallet ${wallet.name}: $e'); + continue; + } + } + + return allV4Farms; + } catch (e) { + logger.e('Failed to load V4 farms: $e'); + return []; + } + } + + /// Wait until farms are listed (similar to wallets) + Future waitUntilListed() async { + if (_isListed) return; + + await for (final _ in stream.where((farms) => farms.isNotEmpty && _isListed == true)) { + break; + } + } + + /// Add a new farm to the list + Future addFarm(Farm farm) async { + await _mutex.protect(() async { + state = [...state, farm]; + }); + } + + /// Remove a farm from the list + Future removeFarm(int farmId) async { + await _mutex.protect(() async { + state = state.where((farm) => farm.farmId != farmId).toList(); + }); + } + + /// Clear all farms and reset state + void clear() { + _isListed = false; + _farmCache.clear(); + _v3Farms.clear(); + _v4Farms.clear(); + state = []; + } + + /// Get farms by type (V3 vs V4) + List get v3Farms => _v3Farms; + List get v4Farms => _v4Farms; + + void startReloadingFarms() { + _reload = true; + } + + void stopReloadingFarms() { + _reload = false; + } + + void reloadFarms() async { + if (!_reload) return; + if (!_loading && _isListed) { + try { + final wallets = []; + if (state.isNotEmpty) { + await refresh(wallets); + } + } catch (e) { + logger.e('Failed to reload farms: $e'); + } + } + + final refreshInterval = 30; + await Future.delayed(Duration(seconds: refreshInterval)); + if (mounted) reloadFarms(); + } + + Farm? getFarm(int farmId) { + return state.where((farm) => farm.farmId == farmId).firstOrNull; + } +} + +final farmsNotifier = StateNotifierProvider>( + (ref) => FarmsNotifier(), +); diff --git a/app/lib/providers/wallets_provider.dart b/app/lib/providers/wallets_provider.dart index 7820b000..3a0e5c24 100644 --- a/app/lib/providers/wallets_provider.dart +++ b/app/lib/providers/wallets_provider.dart @@ -17,9 +17,14 @@ class WalletsNotifier extends StateNotifier> { bool _reload = true; bool _loading = true; bool _isListed = false; + bool _preloading = false; final Mutex _mutex = Mutex(); + final Map _balanceCache = {}; + final Duration _cacheTimeout = const Duration(minutes: 2); bool get isListed => _isListed; + bool get isPreloading => _preloading; + Future list() async { if (_isListed) return; _loading = true; @@ -30,6 +35,89 @@ class WalletsNotifier extends StateNotifier> { _isListed = true; } + Future refresh() async { + _loading = true; + _isListed = false; + _balanceCache.clear(); + + await _mutex.protect(() async { + state = await listWallets(); + }); + _loading = false; + _isListed = true; + } + + /// Preload wallet data in background for faster page transitions + Future preloadWalletData() async { + if (_preloading || _isListed) return; + _preloading = true; + + try { + final wallets = await listWallets(); + if (wallets.isNotEmpty && !_isListed) { + await _mutex.protect(() async { + state = wallets; + _isListed = true; + }); + + _preloadBalancesInBackground(); + } + } catch (e) { + logger.e('Failed to preload wallet data: $e'); + } finally { + _preloading = false; + } + } + + /// Load balances in background without blocking UI + void _preloadBalancesInBackground() async { + if (state.isEmpty) return; + + final chainUrl = Globals().chainUrl; + final futures = state.map((wallet) async { + final cacheKey = '${wallet.name}_balance'; + final now = DateTime.now(); + + // Check cache first + if (_balanceCache.containsKey(cacheKey)) { + final cacheTime = _balanceCache[cacheKey]!; + if (now.difference(cacheTime) < _cacheTimeout) { + return; + } + } + + try { + final results = await Future.wait([ + TFChainService.getBalance(chainUrl, wallet.tfchainAddress), + StellarService.getTFTBalance(wallet.stellarSecret), + ]); + + final tfchainBalance = results[0].toString() == '0.0' ? '0' : results[0].toString(); + final stellarBalance = results[1].toString(); + + // Update cache + _balanceCache[cacheKey] = now; + // Update wallet if values changed + if (tfchainBalance != wallet.tfchainBalance || + stellarBalance != wallet.stellarBalances['TFT']) { + wallet.stellarBalances['TFT'] = stellarBalance; + wallet.tfchainBalance = tfchainBalance; + } + } catch (e) { + logger.e('Failed to preload balance for ${wallet.name}: $e'); + } + }).toList(); + + await Future.wait(futures); + + // Update state once after all balances are loaded + if (mounted) { + await _mutex.protect(() async { + state = [...state]; + }); + } + } + Future waitUntilListed() async { if (_isListed) return; @@ -84,22 +172,48 @@ class WalletsNotifier extends StateNotifier> { if (!_reload) return; if (!_loading) { final chainUrl = Globals().chainUrl; + final now = DateTime.now(); + await _mutex.protect(() async { final List currentState = state.where((w) => true).toList(); - for (final wallet in currentState) { - final balance = - await TFChainService.getBalance(chainUrl, wallet.tfchainAddress); - final tfchainBalance = - balance.toString() == '0.0' ? '0' : balance.toString(); - final stellarBalance = - await StellarService.getTFTBalance(wallet.stellarSecret); - - if (tfchainBalance != wallet.tfchainBalance || - stellarBalance != wallet.stellarBalances['TFT']) { - wallet.stellarBalances['TFT'] = stellarBalance; - wallet.tfchainBalance = tfchainBalance; + + // Use parallel loading for better performance + final futures = currentState.map((wallet) async { + final cacheKey = '${wallet.name}_balance'; + + // Check cache first to avoid unnecessary API calls + if (_balanceCache.containsKey(cacheKey)) { + final cacheTime = _balanceCache[cacheKey]!; + if (now.difference(cacheTime) < _cacheTimeout) { + return; // Skip if recently cached + } } - } + + try { + // Load both balances in parallel + final results = await Future.wait([ + TFChainService.getBalance(chainUrl, wallet.tfchainAddress), + StellarService.getTFTBalance(wallet.stellarSecret), + ]); + + final tfchainBalance = results[0].toString() == '0.0' ? '0' : results[0].toString(); + final stellarBalance = results[1].toString(); + + // Update cache + _balanceCache[cacheKey] = now; + + if (tfchainBalance != wallet.tfchainBalance || + stellarBalance != wallet.stellarBalances['TFT']) { + wallet.stellarBalances['TFT'] = stellarBalance; + wallet.tfchainBalance = tfchainBalance; + } + } catch (e) { + logger.e('Failed to reload balance for ${wallet.name}: $e'); + } + }).toList(); + + await Future.wait(futures); + if (mounted) { state = currentState; } diff --git a/app/lib/screens/farm_screen.dart b/app/lib/screens/farm_screen.dart index eab79499..4c11eb3e 100644 --- a/app/lib/screens/farm_screen.dart +++ b/app/lib/screens/farm_screen.dart @@ -1,20 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:registrar_client/models/farm.dart' as registrarFarm; -import 'package:registrar_client/models/node.dart' as registrarNode; -import 'package:registrar_client/registrar_client.dart' as registrar; -import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; -import 'package:threebotlogin/services/crypto_service.dart'; -import 'package:threebotlogin/services/gridproxy_service.dart'; -import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:threebotlogin/providers/farms_provider.dart'; import 'package:threebotlogin/widgets/add_farm.dart'; import 'package:threebotlogin/widgets/farm_item.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; + class FarmScreen extends ConsumerStatefulWidget { const FarmScreen({super.key}); @@ -25,193 +19,155 @@ class FarmScreen extends ConsumerStatefulWidget { class _FarmScreenState extends ConsumerState with SingleTickerProviderStateMixin { - registrar.RegistrarClient? registrarClient; - List v3Farms = []; - List v4Farms = []; - List wallets = []; - bool failed = false; - bool loading = true; late final TabController _tabController; + bool loading = true; + bool failed = false; + bool reloadFarms = true; + List farms = []; + late FarmsNotifier farmRef; + @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); - listFarms(); + farmRef = ref.read(farmsNotifier.notifier); + listMyFarms(); + farmRef.startReloadingFarms(); + farmRef.reloadFarms(); } - @override - void dispose() { - super.dispose(); - _tabController.dispose(); - } - - listWallets() async { - await ref.read(walletsNotifier.notifier).waitUntilListed(); - wallets = ref.read(walletsNotifier); - } - - Future listFarms() async { - _setLoadingState(); - + listMyFarms() async { + setState(() { + loading = true; + failed = false; + }); try { - final connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult.contains(ConnectivityResult.none)) { - _handleFailure( - 'No internet connection. Please check your network.', + await ref.read(walletsNotifier.notifier).waitUntilListed(); + final wallets = ref.read(walletsNotifier); + + if (wallets.isNotEmpty) { + await ref.read(farmsNotifier.notifier).list(wallets).timeout( + const Duration(minutes: 2), + onTimeout: () { + throw TimeoutException('Loading farm data timed out'); + }, ); - return; } - await _fetchAllFarmData().timeout( - const Duration(minutes: 2), - onTimeout: () { - throw TimeoutException('Loading farm data timed out'); - }, - ); - - _handleSuccess(); + farms = ref.read(farmsNotifier); } on TimeoutException catch (e) { - _handleFailure('Loading farms timed out. Please check your network.', - error: e); - } on Exception catch (e) { - _handleFailure('Failed to load farms due to an unexpected error.', - error: e); + setState(() { + failed = true; + }); + logger.e('Farm loading timeout: $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Loading farms timed out. Please check your network.', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } catch (e) { + setState(() { + failed = true; + }); + logger.e('Failed to load farms: $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Failed to load farms', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } finally { + setState(() { + loading = false; + }); } } - Future _fetchAllFarmData() async { - v3Farms.clear(); - v4Farms.clear(); - registrarClient = null; - - await listWallets(); - - if (wallets.isEmpty) return; - await listV3FarmsAndNodes(); - await listV4FarmsAndNodes(); - } - - void _setLoadingState() { - setState(() { + Future handleRefresh() async { + try { loading = true; - failed = false; - v3Farms.clear(); - v4Farms.clear(); - registrarClient = null; - }); - } + await ref.read(walletsNotifier.notifier).waitUntilListed(); + final wallets = ref.read(walletsNotifier); - void _handleSuccess() { - setState(() { + if (wallets.isNotEmpty) { + await ref.refresh(farmsNotifier.notifier).refresh(wallets).timeout( + const Duration(minutes: 2), + onTimeout: () { + throw TimeoutException('Refreshing farm data timed out'); + }, + ); + } + return; + } on TimeoutException catch (e) { + failed = true; + logger.e('Farm refresh timeout: $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Refreshing farms timed out. Please check your network.', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } catch (e) { + failed = true; + logger.e('Failed to refresh farms: $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Failed to refresh farms', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } finally { loading = false; - failed = false; - }); - logger.i('Farm data loaded successfully.'); - } - - void _handleFailure(String userMessage, {Object? error}) { - if (mounted) { - final errorSnackbar = SnackBar( - content: Text( - userMessage, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.errorContainer), - ), - duration: const Duration(seconds: 3), - ); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar(errorSnackbar); + setState(() {}); } - - setState(() { - loading = false; - failed = true; - }); } - listV3FarmsAndNodes() async { - final Map twinIdWallets = {}; - await Future.wait(wallets.map((w) async { - final twinId = await getTwinId(w.tfchainSecret); - if (twinId != 0) { - twinIdWallets[twinId] = w; - } - })); - final farmsList = await getFarmsByTwinIds(twinIdWallets.keys.toList()); - v3Farms = await Future.wait(farmsList.map((farm) async { - final nodes = await getNodesByFarmId(farm.farmID); - final wallet = twinIdWallets[farm.twinId]!; - return Farm( - name: farm.name, - walletAddress: farm.stellarAddress, - tfchainWalletSecret: wallet.tfchainSecret, - walletName: wallet.name, - twinId: farm.twinId, - farmId: farm.farmID, - nodes: nodes - .map((node) => Node( - nodeId: node.nodeId, - status: NodeStatus.values.firstWhere( - (e) => - e.toString().toLowerCase() == - 'nodestatus.${node.status}', - ), - country: node.location!.country, - uptime: node.uptime, - )) - .toList(), - ); - }).toList()); + @override + void dispose() { + farmRef.stopReloadingFarms(); + super.dispose(); + _tabController.dispose(); } - Future listV4FarmsAndNodes() async { - registrarClient = registrar.RegistrarClient( - baseUrl: Globals().registrarURL, - mnemonicOrSeed: wallets.first.tfchainSecret); - for (var w in wallets) { - try { - final publicKey = await derivePublicKey(w.tfchainSecret); - final account = - await registrarClient!.accounts.getByPublicKey(publicKey); - final farms = await registrarClient!.farms - .list(registrarFarm.FarmFilter(twinID: account.twinID)); - for (var f in farms) { - final farmNodes = await registrarClient!.nodes - .list(registrarNode.NodeFilter(farmID: f.farmID)); - final nodes = farmNodes.map((n) { - // Convert ISO timestamp to Unix timestamp - final lastSeenDate = DateTime.parse(n.lastSeen); - final unixTimestamp = lastSeenDate.millisecondsSinceEpoch ~/ 1000; - return Node( - nodeId: n.nodeID, - status: n.online ? NodeStatus.Up : NodeStatus.Down, - country: n.location.country, - uptime: (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0, - updatedAt: unixTimestamp, - ); - }).toList(); - v4Farms.addAll(farms.map((f) => Farm( - name: f.farmName, - walletAddress: f.stellarAddress!, - tfchainWalletSecret: w.tfchainSecret, - walletName: w.name, - twinId: f.twinID, - farmId: f.farmID!, - nodes: nodes, - ))); - } - } catch (e) { - continue; - } - } - } - Widget listFarmsWidget(List farms, bool isV4) { + + Widget listFarmsWidget(List farms, bool isV4, List wallets) { if (farms.isEmpty) { return SingleChildScrollView( child: Column( @@ -237,7 +193,7 @@ class _FarmScreenState extends ConsumerState SizedBox( width: MediaQuery.of(context).size.width - 40, child: ElevatedButton( - onPressed: _openAddFarmOverlay, + onPressed: () => _openAddFarmOverlay(wallets), child: Text( 'Create New Farm', style: Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -264,15 +220,19 @@ class _FarmScreenState extends ConsumerState @override Widget build(BuildContext context) { + farms = ref.watch(farmsNotifier); + final wallets = ref.watch(walletsNotifier); + final farmsNotifierInstance = ref.read(farmsNotifier.notifier); + Widget mainWidget; + if (loading) { mainWidget = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(), - const SizedBox(height: 16), + const SizedBox(height: 15), Text( 'Loading Farms...', style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -285,16 +245,20 @@ class _FarmScreenState extends ConsumerState mainWidget = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, children: [ + const SizedBox(height: 15), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Try Again'), onPressed: () { - listFarms(); + setState(() { + farmRef.clear(); + failed = false; + loading = true; + }); + listMyFarms(); }, ), - const SizedBox(height: 16), ], ), ); @@ -323,12 +287,12 @@ class _FarmScreenState extends ConsumerState Expanded( child: TabBarView(controller: _tabController, children: [ RefreshIndicator( - onRefresh: listFarms, - child: listFarmsWidget(v3Farms, false), + onRefresh: handleRefresh, + child: listFarmsWidget(farmsNotifierInstance.v3Farms, false, wallets), ), RefreshIndicator( - onRefresh: listFarms, - child: listFarmsWidget(v4Farms, true), + onRefresh: handleRefresh, + child: listFarmsWidget(farmsNotifierInstance.v4Farms, true, wallets), ), ]), ) @@ -338,7 +302,7 @@ class _FarmScreenState extends ConsumerState return mainWidget; } - _openAddFarmOverlay() { + _openAddFarmOverlay(List wallets) { showModalBottomSheet( isScrollControlled: true, useSafeArea: true, @@ -353,8 +317,6 @@ class _FarmScreenState extends ConsumerState } _addFarm(Farm farm) { - setState(() { - _tabController.index == 0 ? v3Farms.add(farm) : v4Farms.add(farm); - }); + ref.read(farmsNotifier.notifier).addFarm(farm); } } diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index a606d8d8..4b1cc1f4 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -30,6 +30,7 @@ import 'package:threebotlogin/widgets/chat_widget.dart'; import 'package:threebotlogin/widgets/home_card.dart'; import 'package:threebotlogin/widgets/home_logo.dart'; import 'package:threebotlogin/widgets/app_layout.dart'; +import 'package:threebotlogin/services/preloading_service.dart'; import 'package:uni_links/uni_links.dart'; /* Screen shows tab bar and all pages defined in router.dart */ @@ -353,6 +354,13 @@ class _HomeScreenState extends ConsumerState Events().onEvent(PhoneEvent().runtimeType, (PhoneEvent event) { phoneVerification(context); }); + + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + final preloadingService = ref.read(preloadingServiceProvider); + preloadingService.startPreloading(ref); + } + }); } Future initUniLinks() async { diff --git a/app/lib/services/preloading_service.dart b/app/lib/services/preloading_service.dart new file mode 100644 index 00000000..c35ef275 --- /dev/null +++ b/app/lib/services/preloading_service.dart @@ -0,0 +1,77 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/providers/farms_provider.dart'; +import 'package:threebotlogin/providers/wallets_provider.dart'; + +/// Service to preload data in background for faster page transitions +class PreloadingService { + static final PreloadingService _instance = PreloadingService._internal(); + factory PreloadingService() => _instance; + PreloadingService._internal(); + + bool _isPreloading = false; + bool _hasPreloaded = false; + + bool get isPreloading => _isPreloading; + bool get hasPreloaded => _hasPreloaded; + + /// Start preloading wallet and farm data in background + Future startPreloading(WidgetRef ref) async { + if (_isPreloading || _hasPreloaded) return; + + _isPreloading = true; + logger.i('Starting background data preloading...'); + + try { + final walletsNotifierInstance = ref.read(walletsNotifier.notifier); + await walletsNotifierInstance.preloadWalletData(); + + final wallets = ref.read(walletsNotifier); + if (wallets.isNotEmpty) { + final farmsNotifierInstance = ref.read(farmsNotifier.notifier); + await farmsNotifierInstance.preloadFarmData(wallets); + } + + _hasPreloaded = true; + logger.i('Background data preloading completed successfully'); + } catch (e) { + logger.e('Failed to preload data: $e'); + } finally { + _isPreloading = false; + } + } + + Future preloadWallets(WidgetRef ref) async { + try { + final walletsNotifierInstance = ref.read(walletsNotifier.notifier); + await walletsNotifierInstance.preloadWalletData(); + } catch (e) { + logger.e('Failed to preload wallets: $e'); + } + } + + Future preloadFarms(WidgetRef ref) async { + try { + final wallets = ref.read(walletsNotifier); + if (wallets.isNotEmpty) { + final farmsNotifierInstance = ref.read(farmsNotifier.notifier); + await farmsNotifierInstance.preloadFarmData(wallets); + } + } catch (e) { + logger.e('Failed to preload farms: $e'); + } + } + + void reset() { + _hasPreloaded = false; + _isPreloading = false; + } + + bool shouldPreload() { + return !_hasPreloaded && !_isPreloading; + } +} + +final preloadingServiceProvider = Provider((ref) { + return PreloadingService(); +}); diff --git a/app/lib/widgets/market/order_card.dart b/app/lib/widgets/market/order_card.dart index f5556db6..6f4b13ba 100644 --- a/app/lib/widgets/market/order_card.dart +++ b/app/lib/widgets/market/order_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/offer.dart'; +// ignore: depend_on_referenced_packages import 'package:intl/intl.dart'; import 'package:threebotlogin/models/wallet.dart' as Wallet; import 'package:threebotlogin/screens/market/order_details.dart';