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,256 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/asset_logo_widget.dart';
import '../../../../shared/widgets/favorite_star_button.dart';
import '../../bloc/header/asset_header_bloc.dart';
import '../../bloc/header/asset_header_state.dart';
import '../../models/asset_model.dart';
class AssetHeroHeader extends StatelessWidget {
final String symbol;
final void Function(String exchange, String ticker)? onExchangeChanged;
final VoidCallback? onForceRefresh;
final String? selectedExchange;
const AssetHeroHeader({
super.key,
required this.symbol,
this.onExchangeChanged,
this.onForceRefresh,
this.selectedExchange,
});
@override
Widget build(BuildContext context) {
final theme = AppTheme.activePreset;
return BlocBuilder<AssetHeaderBloc, AssetHeaderState>(
builder: (context, state) {
String name = symbol;
double? price;
String currency = 'EUR';
String currentExchange = selectedExchange ?? 'XETRA';
List<AssetTickerOption> tickerOptions = [
AssetTickerOption(ticker: 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: 0.0)
];
AssetModel? asset;
if (state is AssetHeaderLoaded) {
asset = state.data;
} else if (state is AssetHeaderLoading) {
asset = state.previousData;
}
if (asset != null) {
name = asset.name.isNotEmpty ? asset.name : symbol;
currency = asset.currency.isNotEmpty ? asset.currency : 'EUR';
price = asset.currentPrice;
currentExchange = selectedExchange ?? asset.exchange;
if (asset.tickers.isNotEmpty) {
tickerOptions = asset.tickers;
}
}
final selectedOption = tickerOptions.firstWhere(
(t) => t.exchange == currentExchange,
orElse: () => tickerOptions.first,
);
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
decoration: BoxDecoration(
color: theme.cardSurface,
border: Border(bottom: BorderSide(color: theme.glassBorder)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
if (Navigator.canPop(context)) ...[
IconButton(
tooltip: 'Zurück',
icon: Icon(Icons.arrow_back, color: theme.textPrimary),
onPressed: () => Navigator.maybePop(context),
),
const SizedBox(width: 8),
],
AssetLogoWidget(symbolOrName: symbol, size: 48),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
name,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: theme.textPrimary,
letterSpacing: 0.5,
),
),
const SizedBox(height: 2),
SelectableText(
symbol,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
letterSpacing: 1.0,
),
),
],
),
),
],
),
),
Row(
children: [
IconButton(
tooltip: 'Force Refresh Data',
icon: Icon(Icons.refresh, color: theme.primaryColor),
onPressed: onForceRefresh,
),
const SizedBox(width: 8),
FavoriteStarButton(symbol: symbol, identifier: symbol, name: name),
],
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'LIVE PRICE',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
letterSpacing: 1.5,
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SelectableText(
price != null && price > 0 ? price.toStringAsFixed(2) : '---',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: theme.textPrimary,
),
),
const SizedBox(width: 6),
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
selectedOption.tradingCurrency.isNotEmpty ? selectedOption.tradingCurrency : currency,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
),
),
),
],
),
],
),
// Interactive Ticker & Exchange Selector Dropdown
PopupMenuButton<String>(
initialValue: selectedOption.exchange,
tooltip: 'Select Exchange & Ticker',
onSelected: (newExchange) {
if (onExchangeChanged != null) {
final opt = tickerOptions.firstWhere(
(t) => t.exchange == newExchange,
orElse: () => tickerOptions.first,
);
onExchangeChanged!(newExchange, opt.ticker);
}
},
itemBuilder: (context) {
return tickerOptions.map((opt) {
final ex = opt.exchange;
final tick = opt.ticker;
final label = '$tick ($ex)';
final isSelected = ex == currentExchange;
return PopupMenuItem<String>(
value: ex,
child: Row(
children: [
Icon(
Icons.business,
size: 16,
color: isSelected ? theme.primaryColor : theme.textMuted,
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? theme.primaryColor : theme.textPrimary,
),
),
],
),
);
}).toList();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: theme.accentColor.withValues(alpha: 0.4)),
),
child: Row(
children: [
Icon(Icons.business, size: 14, color: theme.accentColor),
const SizedBox(width: 6),
Text(
'${selectedOption.ticker} (${selectedOption.exchange})',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: theme.accentColor,
),
),
const SizedBox(width: 4),
Icon(Icons.arrow_drop_down, size: 16, color: theme.accentColor),
],
),
),
),
],
),
],
),
);
},
);
}
}