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,130 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../theme/app_theme.dart';
import '../utils/asset_utils.dart';
/// Reusable performance-optimized Asset Logo Widget supporting SVG, PNG, gradient fallbacks, and Hero transitions.
class AssetLogoWidget extends StatelessWidget {
static final Set<String> _failedUrls = {};
final String symbolOrName;
final String? imageUrl;
final double size;
final bool enableHero;
const AssetLogoWidget({
super.key,
required this.symbolOrName,
this.imageUrl,
this.size = 32,
this.enableHero = true,
});
@override
Widget build(BuildContext context) {
final rawUrl = imageUrl ?? AssetUtils.getLogoUrl(symbolOrName);
final logoUrl = rawUrl != null && rawUrl.isNotEmpty ? AssetUtils.resolveUrl(rawUrl) : null;
final initial = symbolOrName.isNotEmpty ? symbolOrName[0].toUpperCase() : 'A';
final colors = _getGradientColors(initial);
Widget content;
if (logoUrl != null && logoUrl.isNotEmpty && !_failedUrls.contains(logoUrl)) {
final isSvg = logoUrl.toLowerCase().endsWith('.svg') ||
logoUrl.contains('traderepublic.com') ||
logoUrl.contains('/api/logo/');
content = ClipRRect(
borderRadius: BorderRadius.circular(size * 0.3),
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(size * 0.3),
color: AppTheme.activePreset.glassSurface,
border: Border.all(
color: AppTheme.activePreset.glassBorder,
width: 1,
),
),
padding: EdgeInsets.all(size * 0.1),
child: isSvg
? SvgPicture.network(
logoUrl,
width: size,
height: size,
fit: BoxFit.contain,
placeholderBuilder: (context) => _buildFallback(initial, colors),
errorBuilder: (context, error, stackTrace) {
_failedUrls.add(logoUrl);
return _buildFallback(initial, colors);
},
)
: Image.network(
logoUrl,
width: size,
height: size,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
_failedUrls.add(logoUrl);
return _buildFallback(initial, colors);
},
),
),
);
} else {
content = _buildFallback(initial, colors);
}
if (enableHero && symbolOrName.isNotEmpty) {
return Hero(
tag: 'asset_logo_$symbolOrName',
child: content,
);
}
return content;
}
Widget _buildFallback(String initial, List<Color> colors) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(size * 0.3),
gradient: LinearGradient(
colors: colors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Center(
child: Text(
initial,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: size * 0.45,
decoration: TextDecoration.none,
),
),
),
);
}
List<Color> _getGradientColors(String char) {
final code = char.codeUnitAt(0);
switch (code % 5) {
case 0:
return [AppTheme.activePreset.primaryColor, AppTheme.activePreset.accentColor];
case 1:
return [const Color(0xFF6366F1), const Color(0xFFA855F7)];
case 2:
return [const Color(0xFFEC4899), const Color(0xFFF43F5E)];
case 3:
return [const Color(0xFFF59E0B), const Color(0xFFEF4444)];
default:
return [AppTheme.activePreset.accentColor, const Color(0xFF3B82F6)];
}
}
}
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// Reusable glassmorphic container widget adapting to active ThemePreset.
class GlassContainer extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final double? width;
final double? height;
final double? borderRadius;
final VoidCallback? onTap;
const GlassContainer({
super.key,
required this.child,
this.padding = const EdgeInsets.all(16),
this.margin,
this.width,
this.height,
this.borderRadius,
this.onTap,
});
@override
Widget build(BuildContext context) {
final activeTheme = AppTheme.activePreset;
final effectiveRadius = borderRadius ?? activeTheme.borderRadius;
final body = AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: width,
height: height,
margin: margin,
padding: padding,
decoration: BoxDecoration(
color: activeTheme.glassSurface,
borderRadius: BorderRadius.circular(effectiveRadius),
border: Border.all(color: activeTheme.glassBorder, width: 1),
boxShadow: activeTheme.boxShadows,
),
child: child,
);
if (onTap != null) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(effectiveRadius),
child: body,
);
}
return body;
}
}
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// Smooth Shimmer Loading Skeleton Effect Widget.
class ShimmerLoading extends StatefulWidget {
final double width;
final double height;
final double? borderRadius;
const ShimmerLoading({
super.key,
required this.width,
required this.height,
this.borderRadius,
});
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final activeTheme = AppTheme.activePreset;
final radius = widget.borderRadius ?? activeTheme.borderRadius;
final baseColor = activeTheme.glassSurface;
final highlightColor = activeTheme.glassBorder.withValues(alpha: 0.5);
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
baseColor,
highlightColor,
baseColor,
],
stops: [
(_controller.value - 0.3).clamp(0.0, 1.0),
_controller.value,
(_controller.value + 0.3).clamp(0.0, 1.0),
],
),
),
);
},
);
}
}
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// Reusable pill status badge widget for sentiment, trades, or user roles.
class StatusBadge extends StatelessWidget {
final String label;
final Color color;
final IconData? icon;
const StatusBadge({
super.key,
required this.label,
required this.color,
this.icon,
});
factory StatusBadge.sentiment(String status, {double? score}) {
Color bg = AppTheme.textMuted;
if (status.toUpperCase().contains('POS') || (score != null && score > 0.15)) {
bg = AppTheme.primaryEmerald;
} else if (status.toUpperCase().contains('NEG') || (score != null && score < -0.15)) {
bg = AppTheme.accentRed;
} else if (status.toUpperCase().contains('NEU')) {
bg = AppTheme.accentCyan;
}
return StatusBadge(label: status.toUpperCase(), color: bg);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withValues(alpha: 0.4), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 12, color: color),
const SizedBox(width: 4),
],
Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
);
}
}