feat(app): clean architecture with typed repositories, DTO models and calendar event logos
This commit is contained in:
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -39,11 +40,16 @@ class FavoritesState extends Equatable {
|
||||
}
|
||||
|
||||
class FavoritesCubit extends Cubit<FavoritesState> {
|
||||
final ApiClient apiClient;
|
||||
final FavoritesRepository repository;
|
||||
final SignalRService? signalRService;
|
||||
StreamSubscription<Map<String, dynamic>>? _priceSub;
|
||||
|
||||
FavoritesCubit({required this.apiClient, this.signalRService}) : super(const FavoritesState()) {
|
||||
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) {
|
||||
@@ -58,36 +64,24 @@ class FavoritesCubit extends Cubit<FavoritesState> {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
/// Loads favorite metadata via REST ONLY WHEN NEEDED (e.g. initial load or list mutation).
|
||||
/// Loads favorite metadata via repository ONLY WHEN NEEDED.
|
||||
Future<void> loadFavorites() async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
final ts = DateTime.now().millisecondsSinceEpoch;
|
||||
final res = await apiClient.get('/api/v1/user/favorites?_t=$ts');
|
||||
if (res.statusCode == 200 && res.data is List) {
|
||||
final rawList = res.data as List;
|
||||
final set = <String>{};
|
||||
final dedupMap = <String, FavoriteAssetModel>{};
|
||||
final list = await repository.getFavorites();
|
||||
final set = <String>{};
|
||||
|
||||
for (var item in rawList) {
|
||||
final model = FavoriteAssetModel.fromJson(Map<String, dynamic>.from(item));
|
||||
final key = (model.isin.isNotEmpty ? model.isin : (model.symbol.isNotEmpty ? model.symbol : model.name)).toUpperCase();
|
||||
if (!dedupMap.containsKey(key)) {
|
||||
dedupMap[key] = model;
|
||||
}
|
||||
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: dedupMap.values.toList(),
|
||||
isLoading: false,
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
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));
|
||||
}
|
||||
@@ -132,17 +126,15 @@ class FavoritesCubit extends Cubit<FavoritesState> {
|
||||
|
||||
emit(state.copyWith(favoriteIsins: newSet));
|
||||
|
||||
// Perform API call in background
|
||||
// Perform API call via repository
|
||||
try {
|
||||
if (isCurrentlyFav) {
|
||||
await apiClient.delete('/api/v1/user/favorites/$target');
|
||||
await repository.removeFavorite(target);
|
||||
} else {
|
||||
await apiClient.post('/api/v1/user/favorites/$target');
|
||||
await repository.addFavorite(target);
|
||||
}
|
||||
// Re-sync full list to ensure metadata details are fresh
|
||||
await loadFavorites();
|
||||
} catch (_) {
|
||||
// Revert on error
|
||||
await loadFavorites();
|
||||
}
|
||||
}
|
||||
@@ -159,11 +151,11 @@ class FavoritesCubit extends Cubit<FavoritesState> {
|
||||
}).toList();
|
||||
emit(state.copyWith(favoriteDetails: updatedDetails));
|
||||
|
||||
await apiClient.post('/api/v1/user/favorites/$symbol/ticker?ticker=$ticker');
|
||||
await repository.updateFavoriteTicker(symbol, ticker);
|
||||
await loadFavorites();
|
||||
} catch (_) {
|
||||
// Revert/refresh on error
|
||||
await loadFavorites();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../models/favorite_asset_model.dart';
|
||||
|
||||
/// Clean Architecture Repository for managing user favorite assets.
|
||||
class FavoritesRepository {
|
||||
final ApiClient apiClient;
|
||||
|
||||
const FavoritesRepository({required this.apiClient});
|
||||
|
||||
/// Fetches the user's favorite asset list from the backend.
|
||||
Future<List<FavoriteAssetModel>> getFavorites() async {
|
||||
final ts = DateTime.now().millisecondsSinceEpoch;
|
||||
final res = await apiClient.get('/api/v1/user/favorites?_t=$ts');
|
||||
if (res.statusCode == 200 && res.data is List) {
|
||||
final rawList = res.data as List;
|
||||
final dedupMap = <String, FavoriteAssetModel>{};
|
||||
|
||||
for (var item in rawList) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
final model = FavoriteAssetModel.fromJson(item);
|
||||
final key = (model.isin.isNotEmpty ? model.isin : (model.symbol.isNotEmpty ? model.symbol : model.name)).toUpperCase();
|
||||
if (!dedupMap.containsKey(key)) {
|
||||
dedupMap[key] = model;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dedupMap.values.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Adds an asset to user favorites by ISIN or symbol.
|
||||
Future<bool> addFavorite(String identifier) async {
|
||||
final res = await apiClient.post('/api/v1/user/favorites/$identifier');
|
||||
return res.statusCode == 200 || res.statusCode == 201;
|
||||
}
|
||||
|
||||
/// Removes an asset from user favorites.
|
||||
Future<bool> removeFavorite(String identifier) async {
|
||||
final res = await apiClient.delete('/api/v1/user/favorites/$identifier');
|
||||
return res.statusCode == 200 || res.statusCode == 204;
|
||||
}
|
||||
|
||||
/// Updates the preferred active ticker for a favorite asset.
|
||||
Future<bool> updateFavoriteTicker(String symbolOrIsin, String ticker) async {
|
||||
final res = await apiClient.post('/api/v1/user/favorites/$symbolOrIsin/ticker?ticker=$ticker');
|
||||
return res.statusCode == 200;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user