feat(app): responsive asset detail layout, full width chart, reactive hero header, shimmer loaders and enriched fundamentals

This commit is contained in:
2026-08-14 23:57:03 +02:00
parent f94e3b8164
commit 1d244b338a
22 changed files with 1950 additions and 1074 deletions
@@ -168,10 +168,28 @@ class _CandlestickChartState extends State<CandlestickChart> {
return Listener(
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
setState(() {
final double zoomFactor = pointerSignal.scrollDelta.dy > 0 ? 0.9 : 1.1;
_scale = (_scale * zoomFactor).clamp(0.2, 5.0);
});
GestureBinding.instance.pointerSignalResolver.register(
pointerSignal,
(event) {
if (event is PointerScrollEvent) {
setState(() {
final double localX = event.localPosition.dx;
final double zoomFactor = event.scrollDelta.dy > 0 ? 0.9 : 1.1;
final double newScale = (_scale * zoomFactor).clamp(0.2, 5.0);
final double scaleRatio = newScale / _scale;
// Zoom centered on cursor
_panOffset = localX - (localX - _panOffset) * scaleRatio;
_scale = newScale;
final double updatedCandleSpace = (baseWidth + spacing) * _scale;
final double updatedContentWidth = (widget.candles.length + 15) * updatedCandleSpace;
final double newMinOffset = constraints.maxWidth - updatedContentWidth - 60.0;
_panOffset = _panOffset.clamp(newMinOffset < maxOffset ? newMinOffset : maxOffset, maxOffset);
});
}
},
);
}
},
child: GestureDetector(
@@ -5,6 +5,8 @@ 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 '../../bloc/technical/asset_technical_bloc.dart';
import '../../bloc/technical/asset_technical_state.dart';
import '../../models/asset_model.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -146,45 +148,66 @@ class AssetHeroHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AKTUELLER PREIS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
letterSpacing: 1.5,
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
BlocBuilder<AssetTechnicalBloc, AssetTechnicalState>(
builder: (context, taState) {
double? livePrice = price;
String liveCurrency = selectedOption.tradingCurrency.isNotEmpty
? selectedOption.tradingCurrency
: currency;
if (taState is AssetTechnicalLoaded && taState.data != null) {
if (taState.data!.candles.isNotEmpty) {
final lastClose = taState.data!.candles.last.close;
if (lastClose > 0) {
livePrice = lastClose;
}
}
if (taState.data!.currency.isNotEmpty) {
liveCurrency = taState.data!.currency;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
price != null && price > 0 ? price.toStringAsFixed(2) : '---',
Text(
'AKTUELLER PREIS',
style: TextStyle(
fontSize: 32,
fontSize: 11,
fontWeight: FontWeight.bold,
color: theme.textPrimary,
color: theme.primaryColor,
letterSpacing: 1.5,
),
),
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,
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SelectableText(
livePrice != null && livePrice > 0 ? livePrice.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(
liveCurrency,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
),
),
),
],
),
],
),
],
);
},
),
// Interactive Ticker & Exchange Selector Dropdown
PopupMenuButton<String>(