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,57 @@
import 'package:dio/dio.dart';
import '../services/secure_storage_service.dart';
/// Central HTTP ApiClient backed by Dio with automatic 401 Unauthorized handling.
class ApiClient {
final SecureStorageService _storageService;
late final Dio _dio;
Function()? onUnauthorized;
static const String baseUrl = 'http://localhost:5000';
ApiClient(this._storageService) {
_dio = Dio(
BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
),
);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _storageService.getToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (DioException error, handler) async {
if (error.response?.statusCode == 401) {
await _storageService.clearAll();
onUnauthorized?.call();
}
return handler.next(error);
},
),
);
}
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
return await _dio.get(path, queryParameters: queryParameters);
}
Future<Response> post(String path, {dynamic data}) async {
return await _dio.post(path, data: data);
}
Future<Response> put(String path, {dynamic data}) async {
return await _dio.put(path, data: data);
}
Future<Response> delete(String path) async {
return await _dio.delete(path);
}
}
@@ -0,0 +1,136 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:signalr_core/signalr_core.dart';
import '../services/secure_storage_service.dart';
/// Central Real-Time WebSocket Service utilizing SignalR (`signalr_core`).
/// Connects persistently to `/hubs/health` and `/hubs/favorites-prices` WebSockets.
class SignalRService extends ChangeNotifier {
final SecureStorageService storageService;
HubConnection? _healthConnection;
HubConnection? _favoritesConnection;
bool _isConnected = false;
final _statusController = StreamController<bool>.broadcast();
final _healthController = StreamController<List<Map<String, dynamic>>>.broadcast();
final _favoritePricesController = StreamController<Map<String, dynamic>>.broadcast();
bool get isConnected => _isConnected;
Stream<bool> get connectionStream => _statusController.stream;
Stream<List<Map<String, dynamic>>> get healthStream => _healthController.stream;
Stream<Map<String, dynamic>> get favoritePricesStream => _favoritePricesController.stream;
static const String baseUrl = 'http://localhost:5000';
SignalRService(this.storageService);
Future<void> initSignalR() async {
if (_isConnected) return;
try {
final token = await storageService.getToken();
// 1. Connect SystemHealthHub over WebSockets
_healthConnection = HubConnectionBuilder()
.withUrl(
'$baseUrl/hubs/health',
HttpConnectionOptions(
accessTokenFactory: () async => token,
transport: HttpTransportType.webSockets,
logging: (level, message) {
if (kDebugMode) debugPrint('[SignalR Health WS] $message');
},
),
)
.withAutomaticReconnect()
.build();
_healthConnection!.on('ReceiveSystemHealth', (arguments) {
if (arguments != null && arguments.isNotEmpty) {
try {
final List<dynamic> list = arguments.first as List<dynamic>;
final mappedList = list.map((item) => Map<String, dynamic>.from(item as Map)).toList();
_healthController.add(mappedList);
} catch (e) {
if (kDebugMode) debugPrint('[SignalR Health Parsing Error] $e');
}
}
});
_healthConnection!.onclose((error) {
if (kDebugMode) debugPrint('[SignalR Health WS] Closed: $error');
});
await _healthConnection!.start();
if (kDebugMode) debugPrint('[SignalR Health WS] Connected via WebSocket to /hubs/health.');
// 2. Connect FavoritesPriceHub over WebSockets
_favoritesConnection = HubConnectionBuilder()
.withUrl(
'$baseUrl/hubs/favorites-prices',
HttpConnectionOptions(
accessTokenFactory: () async => token,
transport: HttpTransportType.webSockets,
logging: (level, message) {
if (kDebugMode) debugPrint('[SignalR Favorites WS] $message');
},
),
)
.withAutomaticReconnect()
.build();
_favoritesConnection!.on('ReceiveFavoritePrices', (arguments) {
if (arguments != null && arguments.isNotEmpty) {
try {
final Map<String, dynamic> priceMap = Map<String, dynamic>.from(arguments.first as Map);
_favoritePricesController.add(priceMap);
} catch (e) {
if (kDebugMode) debugPrint('[SignalR Favorites Parsing Error] $e');
}
}
});
await _favoritesConnection!.start();
if (kDebugMode) debugPrint('[SignalR Favorites WS] Connected via WebSocket to /hubs/favorites-prices.');
_isConnected = true;
_statusController.add(true);
notifyListeners();
} catch (e) {
if (kDebugMode) debugPrint('[SignalR WebSocket Connection Error] $e');
_isConnected = false;
_statusController.add(false);
notifyListeners();
}
}
void broadcastHealthUpdate(List<Map<String, dynamic>> healthData) {
_healthController.add(healthData);
notifyListeners();
}
void broadcastFavoritePrices(Map<String, dynamic> priceMap) {
_favoritePricesController.add(priceMap);
notifyListeners();
}
void disconnect() async {
try {
await _healthConnection?.stop();
await _favoritesConnection?.stop();
} catch (_) {}
_isConnected = false;
_statusController.add(false);
notifyListeners();
}
@override
void dispose() {
disconnect();
_statusController.close();
_healthController.close();
_favoritePricesController.close();
super.dispose();
}
}