feat(App): update Finlytic Flutter app UI and blocs
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import '../network/api_client.dart';
|
||||
|
||||
/// Asset utility helpers to map ISIN codes, symbols, and company names to logos and details.
|
||||
class AssetUtils {
|
||||
static final Map<String, String> _isinToNameMap = {
|
||||
'US0378331005': 'Apple Inc.',
|
||||
'US5949181045': 'Microsoft Corp.',
|
||||
'US0231351067': 'Amazon.com Inc.',
|
||||
'US67066G1040': 'NVIDIA Corp.',
|
||||
'US88160R1014': 'Tesla Inc.',
|
||||
'US02079K3059': 'Alphabet Inc.',
|
||||
'US30303M1027': 'Meta Platforms',
|
||||
'DE0007164600': 'SAP SE',
|
||||
'DE0007236101': 'Siemens AG',
|
||||
'DE0008469008': 'Allianz SE',
|
||||
'FR0004125920': 'Amundi',
|
||||
};
|
||||
|
||||
static final Map<String, String> _nameToIsinMap = {
|
||||
'APPLE INC.': 'US0378331005',
|
||||
'APPLE': 'US0378331005',
|
||||
'MICROSOFT CORP.': 'US5949181045',
|
||||
'MICROSOFT': 'US5949181045',
|
||||
'AMAZON.COM INC.': 'US0231351067',
|
||||
'AMAZON': 'US0231351067',
|
||||
'NVIDIA CORP.': 'US67066G1040',
|
||||
'NVIDIA': 'US67066G1040',
|
||||
'TESLA INC.': 'US88160R1014',
|
||||
'TESLA': 'US88160R1014',
|
||||
'ALPHABET INC.': 'US02079K3059',
|
||||
'ALPHABET': 'US02079K3059',
|
||||
'META PLATFORMS': 'US30303M1027',
|
||||
'META': 'US30303M1027',
|
||||
'SAP SE': 'DE0007164600',
|
||||
'SAP': 'DE0007164600',
|
||||
'SIEMENS AG': 'DE0007236101',
|
||||
'SIEMENS': 'DE0007236101',
|
||||
'ALLIANZ SE': 'DE0008469008',
|
||||
'ALLIANZ': 'DE0008469008',
|
||||
'AMUNDI': 'FR0004125920',
|
||||
};
|
||||
|
||||
static final Map<String, String> _imageMap = {};
|
||||
|
||||
/// Registers an ISIN, Name, and optional Logo Image URL.
|
||||
static void registerAsset(String isin, String name, [String? imageUrl]) {
|
||||
final cleanIsin = isin.trim().toUpperCase();
|
||||
final cleanName = name.trim();
|
||||
if (cleanIsin.isNotEmpty && cleanName.isNotEmpty) {
|
||||
_isinToNameMap[cleanIsin] = cleanName;
|
||||
_nameToIsinMap[cleanName.toUpperCase()] = cleanIsin;
|
||||
}
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
final resolved = resolveUrl(imageUrl);
|
||||
if (cleanIsin.isNotEmpty) _imageMap[cleanIsin] = resolved;
|
||||
if (cleanName.isNotEmpty) _imageMap[cleanName.toUpperCase()] = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves relative logo URLs (/api/logo/...) to complete backend endpoints.
|
||||
static String resolveUrl(String url) {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
if (url.startsWith('/')) {
|
||||
return '${ApiClient.baseUrl}$url';
|
||||
}
|
||||
return '${ApiClient.baseUrl}/$url';
|
||||
}
|
||||
|
||||
/// Resolves an ISIN or symbol to readable asset name.
|
||||
static String getAssetName(String isinOrSymbol) {
|
||||
final key = isinOrSymbol.trim().toUpperCase();
|
||||
if (_isinToNameMap.containsKey(key)) {
|
||||
return _isinToNameMap[key]!;
|
||||
}
|
||||
return isinOrSymbol;
|
||||
}
|
||||
|
||||
/// Resolves an asset name or symbol to ISIN.
|
||||
static String? getIsin(String nameOrSymbol) {
|
||||
final key = nameOrSymbol.trim().toUpperCase();
|
||||
if (_isinToNameMap.containsKey(key)) return key;
|
||||
return _nameToIsinMap[key];
|
||||
}
|
||||
|
||||
/// Returns official local backend logo URL for given symbol/name/ISIN.
|
||||
static String? getLogoUrl(String symbolOrName) {
|
||||
final key = symbolOrName.trim().toUpperCase();
|
||||
if (_imageMap.containsKey(key)) return resolveUrl(_imageMap[key]!);
|
||||
|
||||
final isin = getIsin(key) ?? (key.length == 12 ? key : null);
|
||||
if (isin != null && isin.length == 12) {
|
||||
return '${ApiClient.baseUrl}/api/logo/$isin';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Formatting utility helpers for currency, percentages, dates, and numbers.
|
||||
class Formatters {
|
||||
static final NumberFormat _currencyFormat = NumberFormat.currency(symbol: '\$', decimalDigits: 2);
|
||||
static final NumberFormat _percentFormat = NumberFormat.decimalPercentPattern(decimalDigits: 2);
|
||||
|
||||
/// Formats currency values (e.g., $1,234.56).
|
||||
static String formatCurrency(double? value, {String symbol = '\$'}) {
|
||||
if (value == null) return '-';
|
||||
if (symbol == '\$') return _currencyFormat.format(value);
|
||||
return NumberFormat.currency(symbol: symbol, decimalDigits: 2).format(value);
|
||||
}
|
||||
|
||||
/// Formats percentage value with sign (e.g. +3.45%).
|
||||
static String formatPercent(double? value) {
|
||||
if (value == null) return '0.00%';
|
||||
final formatted = _percentFormat.format(value / 100);
|
||||
return value >= 0 ? '+$formatted' : formatted;
|
||||
}
|
||||
|
||||
/// Formats compact numbers (e.g. 1.2M, 3.4B).
|
||||
static String formatCompactNumber(double? value) {
|
||||
if (value == null) return '-';
|
||||
return NumberFormat.compact().format(value);
|
||||
}
|
||||
|
||||
/// Formats ISO date time string to readable short date.
|
||||
static String formatDate(String? isoDate) {
|
||||
if (isoDate == null || isoDate.isEmpty) return '-';
|
||||
try {
|
||||
final dt = DateTime.parse(isoDate).toLocal();
|
||||
return DateFormat('dd.MM.yyyy HH:mm').format(dt);
|
||||
} catch (_) {
|
||||
return isoDate;
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats date to day-month format.
|
||||
static String formatShortDate(DateTime dt) {
|
||||
return DateFormat('dd. MMM yyyy').format(dt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/// Universal time formatting helper to format relative timestamps cleanly for UI displays.
|
||||
class TimeUtils {
|
||||
/// Converts ISO-8601 or DateTime inputs to localized, readable relative time strings.
|
||||
///
|
||||
/// Examples:
|
||||
/// - Under 60 minutes: "Vor 15 Min.", "Vor 42 Min."
|
||||
/// - 1 hour or more: "Vor 1 Std.", "Vor 1 Std. 15 Min.", "Vor 3 Std. 45 Min."
|
||||
/// - Yesterday: "Gestern"
|
||||
/// - Older: "22.07.2026"
|
||||
static String formatRelativeTime(dynamic rawDate) {
|
||||
if (rawDate == null) return '';
|
||||
final str = rawDate.toString().trim();
|
||||
if (str.isEmpty) return '';
|
||||
|
||||
DateTime dt;
|
||||
try {
|
||||
dt = DateTime.parse(str).toLocal();
|
||||
} catch (_) {
|
||||
// Return raw string if already formatted or non-date string
|
||||
return str;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final rawDiff = now.difference(dt);
|
||||
|
||||
// If the timestamp is in the future (e.g. slight clock skew or scraper timezone issues), cap it to 0
|
||||
final duration = rawDiff.isNegative ? Duration.zero : rawDiff;
|
||||
final totalMinutes = duration.inMinutes;
|
||||
|
||||
if (totalMinutes < 1) {
|
||||
return 'Vor 1 Min.';
|
||||
}
|
||||
|
||||
if (totalMinutes < 60) {
|
||||
return 'Vor $totalMinutes Min.';
|
||||
}
|
||||
|
||||
final hours = duration.inHours;
|
||||
if (hours < 24) {
|
||||
final remainingMinutes = totalMinutes % 60;
|
||||
if (remainingMinutes == 0) {
|
||||
return 'Vor $hours Std.';
|
||||
} else {
|
||||
return 'Vor $hours Std. $remainingMinutes Min.';
|
||||
}
|
||||
}
|
||||
|
||||
final days = duration.inDays;
|
||||
if (days == 1) {
|
||||
return 'Gestern';
|
||||
} else if (days < 7) {
|
||||
return 'Vor $days Tagen';
|
||||
}
|
||||
|
||||
final dayStr = dt.day.toString().padLeft(2, '0');
|
||||
final monthStr = dt.month.toString().padLeft(2, '0');
|
||||
return '$dayStr.$monthStr.${dt.year}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user