import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/signalr_service.dart'; import '../models/favorite_asset_model.dart'; import '../repositories/favorites_repository.dart'; class FavoritesState extends Equatable { final Set favoriteIsins; final List favoriteDetails; final bool isLoading; const FavoritesState({ this.favoriteIsins = const {}, this.favoriteDetails = const [], this.isLoading = false, }); bool isFavorite(String identifier) { if (identifier.isEmpty) return false; final upper = identifier.toUpperCase(); return favoriteIsins.contains(upper); } FavoritesState copyWith({ Set? favoriteIsins, List? favoriteDetails, bool? isLoading, }) { return FavoritesState( favoriteIsins: favoriteIsins ?? this.favoriteIsins, favoriteDetails: favoriteDetails ?? this.favoriteDetails, isLoading: isLoading ?? this.isLoading, ); } @override List get props => [favoriteIsins, favoriteDetails, isLoading]; } class FavoritesCubit extends Cubit { final FavoritesRepository repository; final SignalRService? signalRService; StreamSubscription>? _priceSub; FavoritesCubit({ FavoritesRepository? repository, ApiClient? apiClient, this.signalRService, }) : repository = repository ?? FavoritesRepository(apiClient: apiClient!), super(const FavoritesState()) { // 1. Subscribe to SignalR 10-second WebSocket price stream if (signalRService != null) { _priceSub = signalRService!.favoritePricesStream.listen((priceMap) { updatePrices(priceMap); }); } } @override Future close() { _priceSub?.cancel(); return super.close(); } /// Loads favorite metadata via repository ONLY WHEN NEEDED. Future loadFavorites() async { emit(state.copyWith(isLoading: true)); try { final list = await repository.getFavorites(); final set = {}; for (var model in list) { if (model.isin.isNotEmpty) set.add(model.isin.toUpperCase()); if (model.symbol.isNotEmpty) set.add(model.symbol.toUpperCase()); if (model.name.isNotEmpty) set.add(model.name.toUpperCase()); } emit(FavoritesState( favoriteIsins: set, favoriteDetails: list, isLoading: false, )); } catch (_) { emit(state.copyWith(isLoading: false)); } } /// Updates current prices and daily growth % live in-memory when SignalR WebSocket payload arrives. void updatePrices(Map priceMap) { if (priceMap.isEmpty) return; final updatedDetails = state.favoriteDetails.map((model) { final keyIsin = model.isin.toUpperCase(); final keySymbol = model.symbol.toUpperCase(); dynamic priceData = priceMap[keyIsin] ?? priceMap[keySymbol]; if (priceData != null && priceData is Map) { final double price = (priceData['currentPrice'] ?? priceData['price'] ?? model.currentPrice).toDouble(); final double change = (priceData['dailyChangePercent'] ?? priceData['change24h'] ?? model.change24h).toDouble(); return model.copyWith(currentPrice: price, change24h: change); } return model; }).toList(); emit(state.copyWith(favoriteDetails: updatedDetails)); } Future toggleFavorite(String identifier, {String? symbol, String? name}) async { if (identifier.isEmpty) return; final target = identifier.toUpperCase(); final isCurrentlyFav = state.isFavorite(target); // Optimistic UI update final newSet = Set.from(state.favoriteIsins); if (isCurrentlyFav) { newSet.remove(target); if (symbol != null) newSet.remove(symbol.toUpperCase()); if (name != null) newSet.remove(name.toUpperCase()); } else { newSet.add(target); if (symbol != null) newSet.add(symbol.toUpperCase()); if (name != null) newSet.add(name.toUpperCase()); } emit(state.copyWith(favoriteIsins: newSet)); // Perform API call via repository try { if (isCurrentlyFav) { await repository.removeFavorite(target); } else { await repository.addFavorite(target); } await loadFavorites(); } catch (_) { await loadFavorites(); } } Future updateFavoriteTicker(String symbol, String ticker) async { try { // Optimistic UI update final target = symbol.toUpperCase(); final updatedDetails = state.favoriteDetails.map((model) { if (model.isin.toUpperCase() == target || model.symbol.toUpperCase() == target) { return model.copyWith(symbol: ticker); } return model; }).toList(); emit(state.copyWith(favoriteDetails: updatedDetails)); await repository.updateFavoriteTicker(symbol, ticker); await loadFavorites(); } catch (_) { await loadFavorites(); } } }