162 lines
5.0 KiB
Dart
162 lines
5.0 KiB
Dart
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<String> favoriteIsins;
|
|
final List<FavoriteAssetModel> 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<String>? favoriteIsins,
|
|
List<FavoriteAssetModel>? favoriteDetails,
|
|
bool? isLoading,
|
|
}) {
|
|
return FavoritesState(
|
|
favoriteIsins: favoriteIsins ?? this.favoriteIsins,
|
|
favoriteDetails: favoriteDetails ?? this.favoriteDetails,
|
|
isLoading: isLoading ?? this.isLoading,
|
|
);
|
|
}
|
|
|
|
@override
|
|
List<Object?> get props => [favoriteIsins, favoriteDetails, isLoading];
|
|
}
|
|
|
|
class FavoritesCubit extends Cubit<FavoritesState> {
|
|
final FavoritesRepository repository;
|
|
final SignalRService? signalRService;
|
|
StreamSubscription<Map<String, dynamic>>? _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<void> close() {
|
|
_priceSub?.cancel();
|
|
return super.close();
|
|
}
|
|
|
|
/// Loads favorite metadata via repository ONLY WHEN NEEDED.
|
|
Future<void> loadFavorites() async {
|
|
emit(state.copyWith(isLoading: true));
|
|
try {
|
|
final list = await repository.getFavorites();
|
|
final set = <String>{};
|
|
|
|
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<String, dynamic> 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<void> 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<String>.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<void> 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();
|
|
}
|
|
}
|
|
}
|
|
|