feat(App): update Finlytic Flutter app UI and blocs
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user