diff --git a/lib/app.dart b/lib/app.dart index 858da9d..147b2d4 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -42,114 +42,60 @@ class _YnabAppState extends ConsumerState { return Scaffold( backgroundColor: kSurfaceBg, + bottomNavigationBar: NavigationBar( + selectedIndex: selectedIndex, + onDestinationSelected: (int index) { + ref.read(_selectedIndexProvider.notifier).state = index; + }, + destinations: _navItems.map((item) { + return NavigationDestination( + icon: Icon(item.icon), + selectedIcon: Icon(item.selectedIcon), + label: item.label, + ); + }).toList(), + ), body: SafeArea( - child: Row( + child: Column( children: [ - // ── Side Navigation ── + // Header Container( - width: 72, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( - color: kSurfaceCard, + color: kSurfaceBg, border: Border( - right: BorderSide(color: kDivider, width: 1), + bottom: BorderSide(color: kDivider, width: 0.5), ), ), - child: Column( + child: Row( children: [ - const SizedBox(height: 20), - // App logo / icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: kGreenAccent.withAlpha(30), - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon(Icons.currency_bitcoin, color: kGreenAccent, size: 22), - ), - ), - const SizedBox(height: 8), Text( - 'YNAB\nLite', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 9, - color: kGreenAccent, - fontWeight: FontWeight.w700, - height: 1.2, + _navItems[selectedIndex].label, + style: const TextStyle( + color: kTextPrimary, + fontSize: 18, + fontWeight: FontWeight.w600, ), ), - const SizedBox(height: 28), - // Navigation items - ...List.generate(_navItems.length, (i) { - final item = _navItems[i]; - final active = i == selectedIndex; - return _NavRailItem( - icon: active ? item.selectedIcon : item.icon, - label: item.label, - active: active, - onTap: () => ref.read(_selectedIndexProvider.notifier).state = i, - ); - }), const Spacer(), - // Settings at bottom - _NavRailItem( - icon: Icons.settings_outlined, - selectedIcon: Icons.settings, - label: 'Paramètres', - active: false, - onTap: () {}, + IconButton( + icon: const Icon(Icons.add, color: kGreenAccent), + onPressed: () {}, + tooltip: 'Ajouter une transaction', + ), + IconButton( + icon: const Icon(Icons.search, color: kTextSecondary), + onPressed: () {}, + tooltip: 'Rechercher', ), - const SizedBox(height: 20), ], ), ), - // ── Main Content ── + // Content Expanded( - child: Column( - children: [ - // Header - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - decoration: BoxDecoration( - color: kSurfaceBg, - border: Border( - bottom: BorderSide(color: kDivider, width: 0.5), - ), - ), - child: Row( - children: [ - Text( - _navItems[selectedIndex].label, - style: const TextStyle( - color: kTextPrimary, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.add, color: kGreenAccent), - onPressed: () {}, - tooltip: 'Ajouter une transaction', - ), - IconButton( - icon: const Icon(Icons.search, color: kTextSecondary), - onPressed: () {}, - tooltip: 'Rechercher', - ), - ], - ), - ), - // Content - Expanded( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: _screens[selectedIndex], - ), - ), - ], + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _screens[selectedIndex], ), ), ], @@ -166,53 +112,3 @@ class _NavItem { const _NavItem({required this.icon, required this.selectedIcon, required this.label}); } -class _NavRailItem extends StatelessWidget { - final IconData icon; - final IconData? selectedIcon; - final String label; - final bool active; - final VoidCallback onTap; - - const _NavRailItem({ - required this.icon, - this.selectedIcon, - required this.label, - required this.active, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Tooltip( - message: label, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - width: 56, - height: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: active ? kGreenAccent.withAlpha(20) : null, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: 22, - color: active ? kGreenAccent : kTextSecondary, - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/data/db.dart b/lib/data/db.dart index 183b659..db92896 100644 --- a/lib/data/db.dart +++ b/lib/data/db.dart @@ -14,7 +14,7 @@ part 'db.g.dart'; ], ) class AppDatabase extends _$AppDatabase { - AppDatabase() : super(_openConnection()); + AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection()); static QueryExecutor _openConnection() { return driftDatabase(name: 'ynab_lite'); diff --git a/lib/main.dart b/lib/main.dart index 69c72da..5f2d868 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,9 @@ class YnabLiteApp extends StatelessWidget { return MaterialApp( title: 'YNAB Lite', debugShowCheckedModeBanner: false, - theme: ynabDarkTheme, + theme: ynabTheme, + darkTheme: ynabDarkTheme, + themeMode: ThemeMode.system, home: const YnabApp(), ); } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 711c428..e1b96c8 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -2,23 +2,23 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; // ── Couleurs custom ─────────────────────────────────────────────────────────── -// Fond très sombre mais pas noir, vert fluo pour accent, elevation uniquement. -const kSurfaceBg = Color(0xFF12161C); -const kSurfaceCard = Color(0xFF1A1E26); -const kSurfaceElevated = Color(0xFF232730); -const kGreenAccent = Color(0xFF00E676); -const kGreenAccentDim = Color(0xFF00B85C); -const kRedAccent = Color(0xFFFF5252); -const kTextPrimary = Color(0xFFE8EAED); -const kTextSecondary = Color(0xFF9AA0A6); -const kDivider = Color(0xFF2C3038); +// Couleurs custom pour le thème vert sage (Light Theme) +const kSurfaceBg = Color(0xFFEDF1E7); // Fond vert très clair (de l'image) +const kSurfaceCard = Color(0xFFF6F9F6); // Fond de carte off-white +const kSurfaceElevated = Color(0xFFCAD8C3); // Sage green pour les surfaces surélevées +const kGreenAccent = Color(0xFF3B5238); // Vert sombre pour le texte et les accents +const kGreenAccentDim = Color(0xFF556B52); +const kRedAccent = Color(0xFFBA1A1A); // Rouge d'erreur Material 3 +const kTextPrimary = Color(0xFF191D17); // Texte principal sombre +const kTextSecondary = Color(0xFF43493E); // Texte secondaire gris-vert +const kDivider = Color(0xFFDDE4D7); // Séparateurs clairs -final ThemeData ynabDarkTheme = ThemeData( +final ThemeData ynabTheme = ThemeData( useMaterial3: true, - brightness: Brightness.dark, + brightness: Brightness.light, colorScheme: ColorScheme.fromSeed( - seedColor: kGreenAccent, - brightness: Brightness.dark, + seedColor: const Color(0xFFCAD8C3), + brightness: Brightness.light, surface: kSurfaceBg, primary: kGreenAccent, secondary: kGreenAccentDim, @@ -30,27 +30,35 @@ final ThemeData ynabDarkTheme = ThemeData( scaffoldBackgroundColor: kSurfaceBg, cardTheme: CardThemeData( color: kSurfaceCard, - elevation: 2, + elevation: 0, // M3 utilise plutôt les tonalités et outline qu'une grosse élévation shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), + side: BorderSide(color: kDivider, width: 1), ), clipBehavior: Clip.antiAlias, ), - navigationRailTheme: NavigationRailThemeData( + navigationBarTheme: NavigationBarThemeData( backgroundColor: kSurfaceCard, - indicatorColor: kGreenAccent.withAlpha(40), - unselectedIconTheme: const IconThemeData(color: kTextSecondary), - selectedIconTheme: const IconThemeData(color: kGreenAccent), - unselectedLabelTextStyle: GoogleFonts.inter( - color: kTextSecondary, - fontSize: 11, - ), - selectedLabelTextStyle: GoogleFonts.inter( - color: kGreenAccent, - fontSize: 11, - fontWeight: FontWeight.w600, - ), - labelType: NavigationRailLabelType.none, + indicatorColor: const Color(0xFFCAD8C3), + iconTheme: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const IconThemeData(color: Color(0xFF191D17)); + } + return const IconThemeData(color: Color(0xFF43493E)); + }), + labelTextStyle: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return GoogleFonts.inter( + color: const Color(0xFF191D17), + fontSize: 12, + fontWeight: FontWeight.w600, + ); + } + return GoogleFonts.inter( + color: const Color(0xFF43493E), + fontSize: 12, + ); + }), ), appBarTheme: AppBarTheme( backgroundColor: kSurfaceBg, @@ -65,7 +73,7 @@ final ThemeData ynabDarkTheme = ThemeData( iconTheme: const IconThemeData(color: kTextSecondary), ), textTheme: GoogleFonts.interTextTheme( - ThemeData.dark().textTheme.copyWith( + ThemeData.light().textTheme.copyWith( headlineLarge: const TextStyle(color: kTextPrimary), headlineMedium: const TextStyle(color: kTextPrimary), headlineSmall: const TextStyle(color: kTextPrimary), @@ -82,17 +90,17 @@ final ThemeData ynabDarkTheme = ThemeData( ), dividerTheme: const DividerThemeData(color: kDivider, thickness: 1), floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: kGreenAccent, - foregroundColor: kSurfaceBg, - elevation: 4, + backgroundColor: const Color(0xFFCAD8C3), + foregroundColor: kTextPrimary, + elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: kSurfaceElevated, + fillColor: kSurfaceCard, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, + borderSide: const BorderSide(color: kDivider), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -118,6 +126,15 @@ final ThemeData ynabDarkTheme = ThemeData( ), ); +final ThemeData ynabDarkTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFFCAD8C3), + brightness: Brightness.dark, + ), +); + // Utils String formatCurrency(double amount) { final sign = amount < 0 ? '-' : ''; diff --git a/test/widget_test.dart b/test/widget_test.dart index 515cace..51db1c1 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -3,19 +3,29 @@ import 'package:ynab_lite/app.dart'; import 'package:ynab_lite/theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ynab_lite/providers/db_provider.dart'; +import 'package:drift/native.dart'; +import 'package:ynab_lite/data/db.dart'; void main() { testWidgets('App renders dashboard by default', (tester) async { + final db = AppDatabase(NativeDatabase.memory()); await tester.pumpWidget( - const ProviderScope( + ProviderScope( + overrides: [ + dbProvider.overrideWithValue(db), + ], child: MaterialApp( - theme: ynabDarkTheme, - home: YnabApp(), + theme: ynabTheme, + home: const YnabApp(), ), ), ); + await tester.pumpAndSettle(); - expect(find.text('Tableau de bord'), findsOneWidget); + expect(find.text('Tableau de bord'), findsNWidgets(2)); expect(find.text('Solde total'), findsOneWidget); + await db.close(); + await tester.pumpAndSettle(); }); }