Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
105 lines
3.1 KiB
Dart
105 lines
3.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// An asset's picture: the issuer's logo from the broker, and while it loads or when the
|
|
/// broker has none, a circle in the brand colour with the icon of the asset class.
|
|
///
|
|
/// The logo URL and colour come from the API as they are; a failed load (offline, a logo
|
|
/// the CDN has since dropped) quietly falls back rather than showing a broken image.
|
|
class AssetIcon extends StatelessWidget {
|
|
const AssetIcon({
|
|
super.key,
|
|
required this.assetClass,
|
|
this.logoUrl,
|
|
this.logoColor,
|
|
this.size = 32,
|
|
});
|
|
|
|
final String? assetClass;
|
|
final String? logoUrl;
|
|
final String? logoColor;
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final url = logoUrl;
|
|
final fallback = _Fallback(
|
|
assetClass: assetClass,
|
|
logoColor: logoColor,
|
|
size: size,
|
|
);
|
|
return SizedBox.square(
|
|
dimension: size,
|
|
child: ClipOval(
|
|
child: url == null
|
|
? fallback
|
|
: Image.network(
|
|
url,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
filterQuality: FilterQuality.medium,
|
|
gaplessPlayback: true,
|
|
errorBuilder: (_, _, _) => fallback,
|
|
loadingBuilder: (_, child, progress) =>
|
|
progress == null ? child : fallback,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Fallback extends StatelessWidget {
|
|
const _Fallback({
|
|
required this.assetClass,
|
|
required this.logoColor,
|
|
required this.size,
|
|
});
|
|
|
|
final String? assetClass;
|
|
final String? logoColor;
|
|
final double size;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
final brand = parseBrandColor(logoColor);
|
|
final background = brand ?? scheme.surfaceContainerHigh;
|
|
final foreground = brand == null
|
|
? scheme.onSurfaceVariant
|
|
: (ThemeData.estimateBrightnessForColor(brand) == Brightness.dark
|
|
? Colors.white
|
|
: Colors.black87);
|
|
return ColoredBox(
|
|
color: background,
|
|
child: Icon(
|
|
assetClassIcon(assetClass),
|
|
size: size * 0.55,
|
|
color: foreground,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// `#21A038` (or `#AA21A038`) as a colour; null for anything else, so a malformed value
|
|
/// from the broker degrades to the neutral fallback instead of throwing.
|
|
Color? parseBrandColor(String? hex) {
|
|
if (hex == null) return null;
|
|
final digits = hex.startsWith('#') ? hex.substring(1) : hex;
|
|
if (digits.length != 6 && digits.length != 8) return null;
|
|
final value = int.tryParse(digits, radix: 16);
|
|
if (value == null) return null;
|
|
return Color(digits.length == 6 ? 0xFF000000 | value : value);
|
|
}
|
|
|
|
IconData assetClassIcon(String? assetClass) => switch (assetClass) {
|
|
'share' => Icons.trending_up,
|
|
'bond' => Icons.receipt_long_outlined,
|
|
'etf' || 'fund' => Icons.pie_chart_outline,
|
|
'currency' => Icons.currency_exchange,
|
|
'index' => Icons.show_chart,
|
|
'deposit' => Icons.savings_outlined,
|
|
'real_estate' => Icons.home_outlined,
|
|
'crypto' => Icons.currency_bitcoin,
|
|
_ => Icons.circle_outlined,
|
|
};
|