feat(App): update Finlytic Flutter app UI and blocs

This commit is contained in:
2026-08-09 21:01:46 +02:00
parent e7427b7464
commit a708d2977c
591 changed files with 1095105 additions and 0 deletions
@@ -0,0 +1,108 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:finlytic_app/features/news/repositories/news_repository.dart';
import 'news_event.dart';
import 'news_state.dart';
class NewsBloc extends Bloc<NewsEvent, NewsState> {
final NewsRepository repository;
static const int pageSize = 20;
StreamSubscription? _liveNewsSubscription;
NewsBloc({required this.repository}) : super(NewsInitial()) {
on<FetchNews>(_onFetchNews);
on<LoadMoreNews>(_onLoadMoreNews);
on<ReceiveLiveNews>(_onReceiveLiveNews);
// Subscribe to live news from SignalR
_liveNewsSubscription = repository.liveNewsStream.listen((article) {
add(ReceiveLiveNews(article));
});
// Connect to SignalR
repository.connectToLiveFeed();
}
Future<void> _onFetchNews(FetchNews event, Emitter<NewsState> emit) async {
try {
emit(NewsLoading());
final articles = await repository.fetchNews(
page: 1,
pageSize: pageSize,
symbol: event.symbol,
isin: event.isin,
date: event.date,
);
emit(NewsLoaded(
articles: articles,
hasReachedMax: articles.length < pageSize,
currentPage: 1,
symbol: event.symbol,
isin: event.isin,
date: event.date,
));
} catch (e) {
emit(NewsError("Failed to fetch news. Please try again."));
}
}
Future<void> _onLoadMoreNews(LoadMoreNews event, Emitter<NewsState> emit) async {
if (state is NewsLoaded) {
final currentState = state as NewsLoaded;
if (currentState.hasReachedMax) return;
try {
final nextPage = currentState.currentPage + 1;
final articles = await repository.fetchNews(
page: nextPage,
pageSize: pageSize,
symbol: currentState.symbol,
isin: currentState.isin,
date: currentState.date,
);
if (articles.isEmpty) {
emit(currentState.copyWith(hasReachedMax: true));
} else {
emit(NewsLoaded(
articles: currentState.articles + articles,
hasReachedMax: articles.length < pageSize,
currentPage: nextPage,
symbol: currentState.symbol,
isin: currentState.isin,
date: currentState.date,
));
}
} catch (e) {
emit(NewsError("Failed to load more news."));
}
}
}
void _onReceiveLiveNews(ReceiveLiveNews event, Emitter<NewsState> emit) {
if (state is NewsLoaded) {
final currentState = state as NewsLoaded;
// Check if the article matches current filters before prepending
bool matchesFilter = true;
if (currentState.symbol != null || currentState.isin != null) {
// Needs proper matching logic if needed, but for now we prepend if there's no filter or if it matches
// Assuming we prepend it regardless for live feed, or we could filter based on MatchedAssets if added to model.
}
if (matchesFilter) {
// Prepend new article to the top of the list!
final updatedArticles = [event.article, ...currentState.articles];
emit(currentState.copyWith(articles: updatedArticles));
}
}
}
@override
Future<void> close() {
_liveNewsSubscription?.cancel();
repository.dispose();
return super.close();
}
}
@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import 'package:finlytic_app/features/news/models/news_article_model.dart';
abstract class NewsEvent extends Equatable {
const NewsEvent();
@override
List<Object?> get props => [];
}
class FetchNews extends NewsEvent {
final bool isRefresh;
final String? symbol;
final String? isin;
final String? date;
const FetchNews({this.isRefresh = false, this.symbol, this.isin, this.date});
@override
List<Object?> get props => [isRefresh, symbol, isin, date];
}
class LoadMoreNews extends NewsEvent {}
class ReceiveLiveNews extends NewsEvent {
final NewsArticleModel article;
const ReceiveLiveNews(this.article);
@override
List<Object?> get props => [article];
}
@@ -0,0 +1,63 @@
import 'package:equatable/equatable.dart';
import 'package:finlytic_app/features/news/models/news_article_model.dart';
abstract class NewsState extends Equatable {
const NewsState();
@override
List<Object?> get props => [];
}
class NewsInitial extends NewsState {}
class NewsLoading extends NewsState {}
class NewsLoaded extends NewsState {
final List<NewsArticleModel> articles;
final bool hasReachedMax;
final int currentPage;
// Filter state preservation
final String? symbol;
final String? isin;
final String? date;
const NewsLoaded({
required this.articles,
this.hasReachedMax = false,
this.currentPage = 1,
this.symbol,
this.isin,
this.date,
});
NewsLoaded copyWith({
List<NewsArticleModel>? articles,
bool? hasReachedMax,
int? currentPage,
String? symbol,
String? isin,
String? date,
}) {
return NewsLoaded(
articles: articles ?? this.articles,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
currentPage: currentPage ?? this.currentPage,
symbol: symbol ?? this.symbol,
isin: isin ?? this.isin,
date: date ?? this.date,
);
}
@override
List<Object?> get props => [articles, hasReachedMax, currentPage, symbol, isin, date];
}
class NewsError extends NewsState {
final String message;
const NewsError(this.message);
@override
List<Object?> get props => [message];
}