From 2686c175f36c66121bbdb56cb1680ccc692df56d Mon Sep 17 00:00:00 2001 From: Blomios Date: Fri, 17 Jul 2026 23:46:26 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20identit=C3=A9=20visuelle=20Court=20?= =?UTF-8?q?Blazer=20sur=20toute=20l'app=20(ticket=20#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute le thème centralisé (presentation/theme.dart) basé sur les polices Anton/Archivo et l'applique à tous les écrans (accueil, bibliothèque d'exercices, programme, séance-modèle, exécution de séance, historique). flutter analyze propre, 27/27 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 --- lib/presentation/exercise_library_screen.dart | 26 +- lib/presentation/game_time_app.dart | 21 +- lib/presentation/history_screen.dart | 20 +- lib/presentation/home_screen.dart | 50 +- lib/presentation/presentation.dart | 1 + lib/presentation/program_screen.dart | 18 +- lib/presentation/theme.dart | 436 ++++++++++++++++++ .../workout_execution_screen.dart | 26 +- lib/presentation/workout_template_screen.dart | 11 +- pubspec.yaml | 7 + 10 files changed, 535 insertions(+), 81 deletions(-) create mode 100644 lib/presentation/theme.dart diff --git a/lib/presentation/exercise_library_screen.dart b/lib/presentation/exercise_library_screen.dart index 3bbdda4..07c9945 100644 --- a/lib/presentation/exercise_library_screen.dart +++ b/lib/presentation/exercise_library_screen.dart @@ -97,7 +97,6 @@ final class _ExerciseLibraryScreenState extends State { decoration: const InputDecoration( labelText: 'Rechercher', prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), ), ), const SizedBox(height: 12), @@ -339,10 +338,7 @@ final class _ExerciseFormScreenState extends State { children: [ TextFormField( controller: _nameController, - decoration: const InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Nom'), textInputAction: TextInputAction.next, validator: (value) => value == null || value.trim().isEmpty ? 'Le nom est obligatoire.' @@ -351,10 +347,7 @@ final class _ExerciseFormScreenState extends State { const SizedBox(height: 12), TextFormField( controller: _descriptionController, - decoration: const InputDecoration( - labelText: 'Description', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Description'), minLines: 2, maxLines: 4, ), @@ -417,10 +410,7 @@ final class _ExerciseFormScreenState extends State { const SizedBox(height: 12), TextFormField( controller: _scoreLabelController, - decoration: const InputDecoration( - labelText: 'Score à saisir', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Score à saisir'), validator: (value) { if (!_hasScore) return null; return value == null || value.trim().isEmpty @@ -431,10 +421,7 @@ final class _ExerciseFormScreenState extends State { const SizedBox(height: 12), TextFormField( controller: _scoreUnitController, - decoration: const InputDecoration( - labelText: 'Unité', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Unité'), validator: (value) { if (!_hasScore) return null; return value == null || value.trim().isEmpty @@ -684,10 +671,7 @@ final class _MediaImportField extends StatelessWidget { : 'Aucun fichier sélectionné' : fileName; return InputDecorator( - decoration: InputDecoration( - labelText: label, - border: const OutlineInputBorder(), - ), + decoration: InputDecoration(labelText: label), child: Row( children: [ Icon(imported ? Icons.check_circle_outline : Icons.perm_media), diff --git a/lib/presentation/game_time_app.dart b/lib/presentation/game_time_app.dart index 379a2b4..d3ff767 100644 --- a/lib/presentation/game_time_app.dart +++ b/lib/presentation/game_time_app.dart @@ -2,21 +2,32 @@ import 'package:flutter/material.dart'; import '../application/app_bootstrap.dart'; import 'home_screen.dart'; +import 'theme.dart'; -final class GameTimeApp extends StatelessWidget { +final class GameTimeApp extends StatefulWidget { const GameTimeApp({required this.bootstrap, super.key}); final AppBootstrap bootstrap; + @override + State createState() => _GameTimeAppState(); +} + +final class _GameTimeAppState extends State { + var _themeMode = ThemeMode.system; + @override Widget build(BuildContext context) { return MaterialApp( title: 'GameTime', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0E7C66)), - useMaterial3: true, + theme: courtBlazerLightTheme(), + darkTheme: courtBlazerDarkTheme(), + themeMode: _themeMode, + home: HomeScreen( + bootstrap: widget.bootstrap, + themeMode: _themeMode, + onThemeModeChanged: (mode) => setState(() => _themeMode = mode), ), - home: HomeScreen(bootstrap: bootstrap), ); } } diff --git a/lib/presentation/history_screen.dart b/lib/presentation/history_screen.dart index 9b29925..f5942c0 100644 --- a/lib/presentation/history_screen.dart +++ b/lib/presentation/history_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import '../application/application.dart'; import '../domain/domain.dart'; +import 'theme.dart'; import 'workout_execution_screen.dart'; final class HistoryListScreen extends StatefulWidget { @@ -62,15 +63,18 @@ final class _HistoryListScreenState extends State { ), ), for (final history in group.value) - ListTile( - title: Text(history.nameSnapshot), - subtitle: Text( - '${_formatDateTime(history.startedAt)} · ' - '${_formatDurationMs(history.totalActiveMs)} · ' - '${_historySummary(history)}', + CourtBlazerAccentPanel( + margin: const EdgeInsets.fromLTRB(12, 0, 12, 8), + child: ListTile( + title: Text(history.nameSnapshot), + subtitle: Text( + '${_formatDateTime(history.startedAt)} · ' + '${_formatDurationMs(history.totalActiveMs)} · ' + '${_historySummary(history)}', + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => _openDetail(history), ), - trailing: const Icon(Icons.chevron_right), - onTap: () => _openDetail(history), ), ], ], diff --git a/lib/presentation/home_screen.dart b/lib/presentation/home_screen.dart index 36b4d0a..a5b9fb5 100644 --- a/lib/presentation/home_screen.dart +++ b/lib/presentation/home_screen.dart @@ -5,13 +5,21 @@ import '../domain/domain.dart'; import 'exercise_library_screen.dart'; import 'history_screen.dart'; import 'program_screen.dart'; +import 'theme.dart'; import 'workout_execution_screen.dart'; import 'workout_template_screen.dart'; final class HomeScreen extends StatefulWidget { - const HomeScreen({required this.bootstrap, super.key}); + const HomeScreen({ + required this.bootstrap, + this.themeMode = ThemeMode.system, + this.onThemeModeChanged, + super.key, + }); final AppBootstrap bootstrap; + final ThemeMode themeMode; + final ValueChanged? onThemeModeChanged; @override State createState() => _HomeScreenState(); @@ -29,7 +37,22 @@ final class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('GameTime')), + appBar: AppBar( + title: const GameTimeLogo(), + actions: [ + PopupMenuButton( + tooltip: 'Thème', + initialValue: widget.themeMode, + icon: const Icon(Icons.contrast), + onSelected: widget.onThemeModeChanged, + itemBuilder: (context) => const [ + PopupMenuItem(value: ThemeMode.system, child: Text('Système')), + PopupMenuItem(value: ThemeMode.light, child: Text('Clair')), + PopupMenuItem(value: ThemeMode.dark, child: Text('Sombre')), + ], + ), + ], + ), body: ListView( children: [ FutureBuilder( @@ -46,17 +69,20 @@ final class _HomeScreenState extends State { setIndex: session.currentSetIndex, ); final exercise = plan.exerciseAt(position); - return MaterialBanner( - content: Text( - 'Séance en cours — Reprendre · ${exercise.name}, ' - 'série ${position.setIndex + 1}/${exercise.setsCount}', - ), - actions: [ - TextButton( - onPressed: () => _resume(session), - child: const Text('Reprendre'), + return CourtBlazerAccentPanel( + margin: const EdgeInsets.all(12), + child: MaterialBanner( + content: Text( + 'Séance en cours — Reprendre · ${exercise.name}, ' + 'série ${position.setIndex + 1}/${exercise.setsCount}', ), - ], + actions: [ + TextButton( + onPressed: () => _resume(session), + child: const Text('Reprendre'), + ), + ], + ), ); }, ), diff --git a/lib/presentation/presentation.dart b/lib/presentation/presentation.dart index 056e35d..af12a86 100644 --- a/lib/presentation/presentation.dart +++ b/lib/presentation/presentation.dart @@ -3,5 +3,6 @@ export 'exercise_library_screen.dart'; export 'history_screen.dart'; export 'home_screen.dart'; export 'program_screen.dart'; +export 'theme.dart'; export 'workout_execution_screen.dart'; export 'workout_template_screen.dart'; diff --git a/lib/presentation/program_screen.dart b/lib/presentation/program_screen.dart index a3b16c7..c0690ac 100644 --- a/lib/presentation/program_screen.dart +++ b/lib/presentation/program_screen.dart @@ -181,10 +181,7 @@ final class _ProgramFormScreenState extends State { children: [ TextFormField( controller: _nameController, - decoration: const InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Nom'), validator: (value) => value == null || value.trim().isEmpty ? 'Le nom est obligatoire.' : null, @@ -194,7 +191,6 @@ final class _ProgramFormScreenState extends State { controller: _defaultRestController, decoration: const InputDecoration( labelText: 'Repos par défaut (s)', - border: OutlineInputBorder(), ), keyboardType: TextInputType.number, validator: _nonNegativeIntValidator, @@ -356,7 +352,6 @@ final class _ExerciseSelectionScreenState decoration: const InputDecoration( labelText: 'Rechercher', prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), ), ), const SizedBox(height: 12), @@ -467,10 +462,7 @@ final class _ProgramExerciseCard extends StatelessWidget { const SizedBox(height: 12), TextFormField( initialValue: '${draft.setsCount}', - decoration: const InputDecoration( - labelText: 'Nombre de séries', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Nombre de séries'), keyboardType: TextInputType.number, onChanged: (value) { draft.setsCount = int.tryParse(value) ?? draft.setsCount; @@ -530,7 +522,6 @@ final class _ProgramExerciseCard extends StatelessWidget { decoration: const InputDecoration( labelText: 'Repos après chaque série (s)', helperText: 'Pas de repos après la toute dernière série.', - border: OutlineInputBorder(), ), keyboardType: TextInputType.number, onChanged: (value) { @@ -571,10 +562,7 @@ final class _NumberField extends StatelessWidget { padding: const EdgeInsets.only(top: 8), child: TextFormField( initialValue: initialValue?.toString(), - decoration: InputDecoration( - labelText: label, - border: const OutlineInputBorder(), - ), + decoration: InputDecoration(labelText: label), keyboardType: TextInputType.number, onChanged: (value) => onChanged(double.tryParse(value)), ), diff --git a/lib/presentation/theme.dart b/lib/presentation/theme.dart new file mode 100644 index 0000000..7b8369f --- /dev/null +++ b/lib/presentation/theme.dart @@ -0,0 +1,436 @@ +import 'package:flutter/material.dart'; + +const _radius = 6.0; +const _badgeRadius = 5.0; + +@immutable +final class CourtBlazerTokens extends ThemeExtension { + const CourtBlazerTokens({ + required this.accent, + required this.border, + required this.mutedText, + required this.success, + }); + + final Color accent; + final Color border; + final Color mutedText; + final Color success; + + @override + CourtBlazerTokens copyWith({ + Color? accent, + Color? border, + Color? mutedText, + Color? success, + }) { + return CourtBlazerTokens( + accent: accent ?? this.accent, + border: border ?? this.border, + mutedText: mutedText ?? this.mutedText, + success: success ?? this.success, + ); + } + + @override + CourtBlazerTokens lerp(ThemeExtension? other, double t) { + if (other is! CourtBlazerTokens) return this; + return CourtBlazerTokens( + accent: Color.lerp(accent, other.accent, t)!, + border: Color.lerp(border, other.border, t)!, + mutedText: Color.lerp(mutedText, other.mutedText, t)!, + success: Color.lerp(success, other.success, t)!, + ); + } +} + +final class AppTextStyles { + const AppTextStyles._(); + + static TextStyle scoreNumber(BuildContext context) { + return Theme.of(context).textTheme.displaySmall!.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ); + } + + static TextStyle timer(BuildContext context) { + return Theme.of(context).textTheme.displayLarge!.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ); + } +} + +CourtBlazerTokens courtBlazerTokensOf(BuildContext context) { + final theme = Theme.of(context); + return theme.extension() ?? + CourtBlazerTokens( + accent: theme.colorScheme.secondary, + border: theme.dividerColor, + mutedText: theme.colorScheme.onSurfaceVariant, + success: Colors.green, + ); +} + +ThemeData courtBlazerLightTheme() { + return _courtBlazerTheme( + brightness: Brightness.light, + primary: const Color(0xFF9E7623), + accent: const Color(0xFFB91C2B), + background: const Color(0xFFF4F1EA), + surface: Colors.white, + text: const Color(0xFF11131A), + mutedText: const Color(0xFF626A78), + border: const Color(0xFFE4DFD2), + success: const Color(0xFF178A4A), + error: const Color(0xFFC81E32), + ); +} + +ThemeData courtBlazerDarkTheme() { + return _courtBlazerTheme( + brightness: Brightness.dark, + primary: const Color(0xFFC9A24A), + accent: const Color(0xFFD72638), + background: const Color(0xFF080A12), + surface: const Color(0xFF141824), + text: const Color(0xFFF5F1E8), + mutedText: const Color(0xFFA7ADBA), + border: const Color(0xFF242A3A), + success: const Color(0xFF2ECC71), + error: const Color(0xFFFF4D5E), + ); +} + +ThemeData _courtBlazerTheme({ + required Brightness brightness, + required Color primary, + required Color accent, + required Color background, + required Color surface, + required Color text, + required Color mutedText, + required Color border, + required Color success, + required Color error, +}) { + final colorScheme = ColorScheme( + brightness: brightness, + primary: primary, + onPrimary: brightness == Brightness.dark ? background : surface, + secondary: accent, + onSecondary: surface, + error: error, + onError: surface, + surface: surface, + onSurface: text, + ); + final baseTextTheme = _textTheme(text, mutedText); + final roundedBorder = RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_radius), + side: BorderSide(color: border), + ); + + return ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: colorScheme, + scaffoldBackgroundColor: background, + fontFamily: 'Archivo', + textTheme: baseTextTheme, + appBarTheme: AppBarTheme( + centerTitle: false, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: background, + foregroundColor: text, + titleTextStyle: baseTextTheme.titleLarge, + ), + cardTheme: CardThemeData( + elevation: 0, + color: surface, + surfaceTintColor: Colors.transparent, + shape: roundedBorder, + margin: EdgeInsets.zero, + ), + chipTheme: ChipThemeData( + backgroundColor: surface, + selectedColor: primary.withAlpha(41), + disabledColor: border, + labelStyle: baseTextTheme.labelMedium, + secondaryLabelStyle: baseTextTheme.labelMedium, + side: BorderSide(color: border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_badgeRadius), + ), + ), + dividerTheme: DividerThemeData(color: border, thickness: 1), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: surface, + border: _inputBorder(border), + enabledBorder: _inputBorder(border), + focusedBorder: _inputBorder(primary, width: 1.5), + errorBorder: _inputBorder(error), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_radius), + ), + textStyle: baseTextTheme.labelLarge, + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + side: BorderSide(color: border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_radius), + ), + textStyle: baseTextTheme.labelLarge, + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom(textStyle: baseTextTheme.labelLarge), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_radius), + ), + ), + listTileTheme: ListTileThemeData( + iconColor: mutedText, + titleTextStyle: baseTextTheme.titleSmall, + subtitleTextStyle: baseTextTheme.bodyMedium?.copyWith(color: mutedText), + ), + bannerTheme: MaterialBannerThemeData( + backgroundColor: surface, + dividerColor: Colors.transparent, + contentTextStyle: baseTextTheme.bodyMedium, + ), + extensions: [ + CourtBlazerTokens( + accent: accent, + border: border, + mutedText: mutedText, + success: success, + ), + ], + ); +} + +TextTheme _textTheme(Color text, Color mutedText) { + const anton = 'Anton'; + const archivo = 'Archivo'; + return TextTheme( + displayLarge: TextStyle( + fontFamily: anton, + fontSize: 56, + height: 1, + color: text, + ), + displayMedium: TextStyle( + fontFamily: anton, + fontSize: 44, + height: 1.05, + color: text, + ), + displaySmall: TextStyle( + fontFamily: anton, + fontSize: 34, + height: 1.05, + color: text, + ), + headlineLarge: TextStyle( + fontFamily: anton, + fontSize: 32, + height: 1.1, + color: text, + ), + headlineMedium: TextStyle( + fontFamily: anton, + fontSize: 28, + height: 1.1, + color: text, + ), + headlineSmall: TextStyle( + fontFamily: anton, + fontSize: 24, + height: 1.15, + color: text, + ), + titleLarge: TextStyle( + fontFamily: anton, + fontSize: 22, + height: 1.15, + color: text, + ), + titleMedium: TextStyle( + fontFamily: anton, + fontSize: 18, + height: 1.2, + color: text, + ), + titleSmall: TextStyle( + fontFamily: archivo, + fontSize: 16, + fontWeight: FontWeight.w700, + color: text, + ), + bodyLarge: TextStyle(fontFamily: archivo, fontSize: 16, color: text), + bodyMedium: TextStyle(fontFamily: archivo, fontSize: 14, color: text), + bodySmall: TextStyle(fontFamily: archivo, fontSize: 12, color: mutedText), + labelLarge: TextStyle( + fontFamily: archivo, + fontSize: 14, + fontWeight: FontWeight.w700, + color: text, + ), + labelMedium: TextStyle( + fontFamily: archivo, + fontSize: 12, + fontWeight: FontWeight.w700, + color: text, + ), + ); +} + +OutlineInputBorder _inputBorder(Color color, {double width = 1}) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(_radius), + borderSide: BorderSide(color: color, width: width), + ); +} + +final class CourtBlazerAccentPanel extends StatelessWidget { + const CourtBlazerAccentPanel({ + required this.child, + this.padding, + this.margin, + super.key, + }); + + final Widget child; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final tokens = courtBlazerTokensOf(context); + return Container( + margin: margin, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(_radius), + border: Border.all(color: tokens.border), + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Container(height: 2, color: tokens.accent), + Padding(padding: padding ?? EdgeInsets.zero, child: child), + ], + ), + ); + } +} + +final class GameTimeLogo extends StatelessWidget { + const GameTimeLogo({this.wordmark = true, this.height = 36, super.key}); + + final bool wordmark; + final double height; + + @override + Widget build(BuildContext context) { + final textStyle = Theme.of(context).textTheme.titleLarge!.copyWith( + fontFamily: 'Anton', + fontSize: height * 0.54, + ); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomPaint( + size: Size(height * 1.22, height), + painter: _GameTimePatchPainter(Theme.of(context)), + ), + if (wordmark) ...[ + const SizedBox(width: 10), + Text('GameTime', style: textStyle), + ], + ], + ); + } +} + +final class _GameTimePatchPainter extends CustomPainter { + const _GameTimePatchPainter(this.theme); + + final ThemeData theme; + + @override + void paint(Canvas canvas, Size size) { + final tokens = + theme.extension() ?? + CourtBlazerTokens( + accent: theme.colorScheme.secondary, + border: theme.dividerColor, + mutedText: theme.colorScheme.onSurfaceVariant, + success: Colors.green, + ); + final rect = Offset.zero & size; + final radius = Radius.circular(size.height * 0.25); + final shape = RRect.fromRectAndRadius(rect.deflate(1), radius); + final background = Paint()..color = theme.colorScheme.surface; + final border = Paint() + ..color = theme.colorScheme.primary + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + canvas.drawRRect(shape, background); + canvas.save(); + canvas.clipRRect(shape); + final stripe = Path() + ..moveTo(size.width * 0.08, size.height) + ..lineTo(size.width * 0.34, size.height) + ..lineTo(size.width * 0.92, 0) + ..lineTo(size.width * 0.66, 0) + ..close(); + canvas.drawPath(stripe, Paint()..color = tokens.accent); + canvas.restore(); + canvas.drawRRect(shape, border); + canvas.drawCircle( + Offset(size.width * 0.22, size.height * 0.25), + size.height * 0.055, + Paint()..color = theme.colorScheme.primary, + ); + + final textPainter = TextPainter( + text: TextSpan( + text: 'GT', + style: TextStyle( + fontFamily: 'Anton', + fontSize: size.height * 0.52, + color: theme.colorScheme.onSurface, + height: 1, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + textPainter.paint( + canvas, + Offset( + (size.width - textPainter.width) / 2, + (size.height - textPainter.height) / 2 + size.height * 0.03, + ), + ); + } + + @override + bool shouldRepaint(covariant _GameTimePatchPainter oldDelegate) { + return oldDelegate.theme != theme; + } +} diff --git a/lib/presentation/workout_execution_screen.dart b/lib/presentation/workout_execution_screen.dart index c907794..f386915 100644 --- a/lib/presentation/workout_execution_screen.dart +++ b/lib/presentation/workout_execution_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../application/application.dart'; import '../domain/domain.dart'; import 'history_screen.dart'; +import 'theme.dart'; enum WorkoutExecutionMode { active, rest, paused, finished } @@ -146,7 +147,7 @@ final class _WorkoutExecutionScreenState extends State { Center( child: Text( _formatDuration(Duration(seconds: _remainingRestSeconds)), - style: Theme.of(context).textTheme.displayLarge, + style: AppTextStyles.timer(context), ), ), if (next != null && nextExercise != null) ...[ @@ -533,7 +534,7 @@ final class SetMeasureInput extends StatelessWidget { exercise.targetTimeSeconds == null ? 'Chronométrer' : '${exercise.targetTimeSeconds} s', - style: Theme.of(context).textTheme.displaySmall, + style: AppTextStyles.scoreNumber(context), ), ], ), @@ -549,7 +550,7 @@ final class SetMeasureInput extends StatelessWidget { onRepsChanged((reps - 1).clamp(0, 999).toInt()), icon: const Icon(Icons.remove), ), - Text('$reps'), + Text('$reps', style: AppTextStyles.scoreNumber(context)), IconButton( onPressed: () => onRepsChanged(reps + 1), icon: const Icon(Icons.add), @@ -561,11 +562,11 @@ final class SetMeasureInput extends StatelessWidget { const SizedBox(height: 12), TextField( controller: scoreController, + style: AppTextStyles.scoreNumber(context), decoration: InputDecoration( labelText: exercise.scoreUnit == null ? 'Score' : 'Score (${exercise.scoreUnit})', - border: const OutlineInputBorder(), ), keyboardType: TextInputType.number, ), @@ -770,13 +771,16 @@ final class _Header extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(elapsedLabel, style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 4), - Text(progressLabel), - ], + return CourtBlazerAccentPanel( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(elapsedLabel, style: AppTextStyles.scoreNumber(context)), + const SizedBox(height: 4), + Text(progressLabel), + ], + ), ); } } diff --git a/lib/presentation/workout_template_screen.dart b/lib/presentation/workout_template_screen.dart index f71490c..9666990 100644 --- a/lib/presentation/workout_template_screen.dart +++ b/lib/presentation/workout_template_screen.dart @@ -205,10 +205,7 @@ final class _WorkoutTemplateFormScreenState children: [ TextFormField( controller: _nameController, - decoration: const InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder(), - ), + decoration: const InputDecoration(labelText: 'Nom'), validator: (value) => value == null || value.trim().isEmpty ? 'Le nom est obligatoire.' : null, @@ -420,7 +417,6 @@ final class _WorkoutTemplateProgramDetailScreenState '${exercise.setsCountOverride ?? exercise.setsCount}', decoration: const InputDecoration( labelText: 'Nombre de séries', - border: OutlineInputBorder(), ), keyboardType: TextInputType.number, onChanged: (value) { @@ -633,10 +629,7 @@ final class _NumberField extends StatelessWidget { padding: const EdgeInsets.only(top: 8), child: TextFormField( initialValue: initialValue?.toString(), - decoration: InputDecoration( - labelText: label, - border: const OutlineInputBorder(), - ), + decoration: InputDecoration(labelText: label), keyboardType: TextInputType.number, onChanged: (value) => onChanged(double.tryParse(value)), ), diff --git a/pubspec.yaml b/pubspec.yaml index 8ae3487..cba2832 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,3 +27,10 @@ dev_dependencies: flutter: uses-material-design: true + fonts: + - family: Anton + fonts: + - asset: assets/fonts/Anton-Regular.ttf + - family: Archivo + fonts: + - asset: assets/fonts/Archivo-Variable.ttf