Files
Dmitry 62d36aa3e8 feat(app): новый визуальный язык, верхняя навигация, портфели, создание счетов и ручные события
Тема и общие виджеты (AssetIcon, ServiceMark, HelpTip, глоссарий), верхняя панель TopNav и вкладки Аналитики вместо NavSidebar, иконки PWA. Экраны: портфели, создание брокерского счёта, ручное событие, правка инструмента, Обзор карточками по скоупам и таблица активов, пересчёт метрик с опросом /metrics/status. design-system.md обновлён.
2026-09-19 22:14:21 +03:00

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,
};