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
+232
View File
@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
class ThemePreset {
final String id;
final String name;
final Brightness brightness;
final Color darkBackground;
final Color cardSurface;
final Color glassSurface;
final Color glassBorder;
final Color primaryColor;
final Color accentColor;
final Color accentRed;
final Color textPrimary;
final Color textSecondary;
final Color textMuted;
final double borderRadius;
final List<BoxShadow> boxShadows;
const ThemePreset({
required this.id,
required this.name,
required this.brightness,
required this.darkBackground,
required this.cardSurface,
required this.glassSurface,
required this.glassBorder,
required this.primaryColor,
required this.accentColor,
required this.accentRed,
required this.textPrimary,
required this.textSecondary,
required this.textMuted,
required this.borderRadius,
required this.boxShadows,
});
ThemeData toThemeData() {
final isDark = brightness == Brightness.dark;
return (isDark ? ThemeData.dark() : ThemeData.light()).copyWith(
scaffoldBackgroundColor: darkBackground,
primaryColor: primaryColor,
colorScheme: isDark
? ColorScheme.dark(
primary: primaryColor,
secondary: accentColor,
surface: cardSurface,
error: accentRed,
)
: ColorScheme.light(
primary: primaryColor,
secondary: accentColor,
surface: cardSurface,
error: accentRed,
),
appBarTheme: AppBarTheme(
backgroundColor: cardSurface,
elevation: isDark ? 0 : 1,
centerTitle: false,
iconTheme: IconThemeData(color: textPrimary),
titleTextStyle: TextStyle(color: textPrimary, fontSize: 18, fontWeight: FontWeight.bold),
),
cardTheme: CardThemeData(
color: cardSurface,
elevation: isDark ? 0 : 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
side: BorderSide(color: glassBorder, width: 1),
),
),
dialogTheme: DialogThemeData(
backgroundColor: cardSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius + 2),
side: BorderSide(color: glassBorder, width: 1),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: glassSurface,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRadius),
borderSide: BorderSide(color: glassBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRadius),
borderSide: BorderSide(color: glassBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRadius),
borderSide: BorderSide(color: primaryColor, width: 1.5),
),
),
);
}
}
/// Dynamic Finlytic Multi-Theme Presets & Tokens Engine.
class AppTheme {
static const ThemePreset darkClassic = ThemePreset(
id: 'dark_classic',
name: 'Dark',
brightness: Brightness.dark,
darkBackground: Color(0xFF0A0E17),
cardSurface: Color(0xFF12182B),
glassSurface: Color(0x2A1A233A),
glassBorder: Color(0x33425980),
primaryColor: Color(0xFF00E676),
accentColor: Color(0xFF00E5FF),
accentRed: Color(0xFFFF5252),
textPrimary: Colors.white,
textSecondary: Color(0xFFB0BEC5),
textMuted: Color(0xFF607D8B),
borderRadius: 14.0,
boxShadows: [
BoxShadow(color: Color(0x1F000000), blurRadius: 10, offset: Offset(0, 4)),
],
);
static const ThemePreset darkCyberNeon = ThemePreset(
id: 'dark_cyber_neon',
name: 'Alternative Dark (Cyber Neon)',
brightness: Brightness.dark,
darkBackground: Color(0xFF0D0B1E),
cardSurface: Color(0xFF161233),
glassSurface: Color(0x3A261D52),
glassBorder: Color(0x5500FFA3),
primaryColor: Color(0xFF00FFA3),
accentColor: Color(0xFFFF007A),
accentRed: Color(0xFFFF2E93),
textPrimary: Color(0xFFF5F3FF),
textSecondary: Color(0xFFC4B5FD),
textMuted: Color(0xFF8B5CF6),
borderRadius: 8.0,
boxShadows: [
BoxShadow(color: Color(0x3300FFA3), blurRadius: 12, spreadRadius: -2),
],
);
static const ThemePreset lightClassic = ThemePreset(
id: 'light_classic',
name: 'Light',
brightness: Brightness.light,
darkBackground: Color(0xFFF8FAFC),
cardSurface: Color(0xFFFFFFFF),
glassSurface: Color(0xFFF1F5F9),
glassBorder: Color(0xFFE2E8F0),
primaryColor: Color(0xFF059669),
accentColor: Color(0xFF0284C7),
accentRed: Color(0xFFDC2626),
textPrimary: Color(0xFF0F172A),
textSecondary: Color(0xFF475569),
textMuted: Color(0xFF94A3B8),
borderRadius: 16.0,
boxShadows: [
BoxShadow(color: Color(0x0F000000), blurRadius: 12, offset: Offset(0, 4)),
],
);
static const ThemePreset lightWarmSand = ThemePreset(
id: 'light_warm_sand',
name: 'Alternative Light (Warm Paper)',
brightness: Brightness.light,
darkBackground: Color(0xFFF5F2EB),
cardSurface: Color(0xFFFFFDF9),
glassSurface: Color(0xFFEFEADF),
glassBorder: Color(0xFFE2D9C8),
primaryColor: Color(0xFFD97706),
accentColor: Color(0xFF2563EB),
accentRed: Color(0xFFE11D48),
textPrimary: Color(0xFF272522),
textSecondary: Color(0xFF57534E),
textMuted: Color(0xFFA8A29E),
borderRadius: 12.0,
boxShadows: [
BoxShadow(color: Color(0x14443422), blurRadius: 8, offset: Offset(0, 3)),
],
);
static const ThemePreset nordicMint = ThemePreset(
id: 'nordic_mint',
name: 'Nordic Mint',
brightness: Brightness.light,
darkBackground: Color(0xFFEFF6F5),
cardSurface: Color(0xFFFFFFFF),
glassSurface: Color(0xFFE0F2FE),
glassBorder: Color(0xFFCCFBF1),
primaryColor: Color(0xFF0D9488),
accentColor: Color(0xFF0284C7),
accentRed: Color(0xFFF43F5E),
textPrimary: Color(0xFF111827),
textSecondary: Color(0xFF374151),
textMuted: Color(0xFF6B7280),
borderRadius: 20.0,
boxShadows: [
BoxShadow(color: Color(0x0D0F766E), blurRadius: 14, offset: Offset(0, 4)),
],
);
static const List<ThemePreset> allPresets = [
darkClassic,
darkCyberNeon,
lightClassic,
lightWarmSand,
nordicMint,
];
static ThemePreset getPresetById(String? id) {
return allPresets.firstWhere(
(p) => p.id.toLowerCase() == id?.toLowerCase(),
orElse: () => darkClassic,
);
}
// Legacy static color mappings for backward compatibility
static Color get darkBackground => activePreset.darkBackground;
static Color get cardSurface => activePreset.cardSurface;
static Color get surfaceDark => activePreset.cardSurface;
static Color get glassSurface => activePreset.glassSurface;
static Color get glassBorder => activePreset.glassBorder;
static Color get primaryEmerald => activePreset.primaryColor;
static Color get accentCyan => activePreset.accentColor;
static Color get accentRed => activePreset.accentRed;
static Color get textPrimary => activePreset.textPrimary;
static Color get textSecondary => activePreset.textSecondary;
static Color get textMuted => activePreset.textMuted;
static ThemePreset activePreset = darkClassic;
static ThemeData get darkTheme => activePreset.toThemeData();
}
@@ -0,0 +1,49 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../network/api_client.dart';
import 'app_theme.dart';
class ThemeState {
final ThemePreset preset;
final bool isLoading;
const ThemeState({required this.preset, this.isLoading = false});
}
class ThemeCubit extends Cubit<ThemeState> {
final ApiClient? apiClient;
ThemeCubit({this.apiClient}) : super(ThemeState(preset: AppTheme.activePreset));
Future<void> fetchUserThemePreference() async {
if (apiClient == null) return;
try {
final res = await apiClient!.get('/api/v1/user/preferences');
if (res.statusCode == 200 && res.data is Map) {
final themeId = res.data['themePreference']?.toString();
if (themeId != null && themeId.isNotEmpty) {
final preset = AppTheme.getPresetById(themeId);
AppTheme.activePreset = preset;
emit(ThemeState(preset: preset));
}
}
} catch (_) {
// Keep active preset on network failure
}
}
Future<void> setTheme(ThemePreset newPreset) async {
AppTheme.activePreset = newPreset;
emit(ThemeState(preset: newPreset));
if (apiClient != null) {
try {
await apiClient!.put(
'/api/v1/user/preferences/theme',
data: {'themeId': newPreset.id},
);
} catch (_) {
// Silently handle offline preference update
}
}
}
}
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'app_theme.dart';
import 'theme_cubit.dart';
/// Modal dialog allowing the user to select and preview theme presets.
class ThemePickerDialog extends StatelessWidget {
const ThemePickerDialog({super.key});
@override
Widget build(BuildContext context) {
final activeTheme = AppTheme.activePreset;
return Dialog(
backgroundColor: activeTheme.cardSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(activeTheme.borderRadius + 2),
side: BorderSide(color: activeTheme.glassBorder, width: 1),
),
child: Container(
width: 480,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.palette_outlined, color: activeTheme.primaryColor),
const SizedBox(width: 10),
const Text('Design & Theme Auswählen', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 16),
Flexible(
child: SingleChildScrollView(
child: Column(
children: AppTheme.allPresets.map((preset) {
final isSelected = activeTheme.id == preset.id;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: () {
context.read<ThemeCubit>().setTheme(preset);
Navigator.pop(context);
},
borderRadius: BorderRadius.circular(preset.borderRadius),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: preset.glassSurface,
borderRadius: BorderRadius.circular(preset.borderRadius),
border: Border.all(
color: isSelected ? preset.primaryColor : preset.glassBorder,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
// Palette Color Preview Dots
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: preset.darkBackground,
shape: BoxShape.circle,
border: Border.all(color: preset.glassBorder),
),
child: Center(
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: preset.primaryColor,
shape: BoxShape.circle,
),
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
preset.name,
style: TextStyle(
color: preset.textPrimary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 2),
Text(
'Ecken: ${preset.borderRadius.toInt()}px • ${preset.brightness == Brightness.dark ? "Dunkel" : "Hell"}',
style: TextStyle(color: preset.textMuted, fontSize: 11),
),
],
),
),
if (isSelected)
Icon(Icons.check_circle_rounded, color: preset.primaryColor, size: 22),
],
),
),
),
);
}).toList(),
),
),
),
],
),
),
);
}
}