feat(ui): identité visuelle Court Blazer sur toute l'app (ticket #17)

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 23:46:26 +02:00
parent 202e9d8db3
commit 2686c175f3
10 changed files with 535 additions and 81 deletions

View File

@ -97,7 +97,6 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Rechercher', labelText: 'Rechercher',
prefixIcon: Icon(Icons.search), prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -339,10 +338,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
children: [ children: [
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Nom'),
labelText: 'Nom',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
validator: (value) => value == null || value.trim().isEmpty validator: (value) => value == null || value.trim().isEmpty
? 'Le nom est obligatoire.' ? 'Le nom est obligatoire.'
@ -351,10 +347,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
TextFormField( TextFormField(
controller: _descriptionController, controller: _descriptionController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Description'),
labelText: 'Description',
border: OutlineInputBorder(),
),
minLines: 2, minLines: 2,
maxLines: 4, maxLines: 4,
), ),
@ -417,10 +410,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
TextFormField( TextFormField(
controller: _scoreLabelController, controller: _scoreLabelController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Score à saisir'),
labelText: 'Score à saisir',
border: OutlineInputBorder(),
),
validator: (value) { validator: (value) {
if (!_hasScore) return null; if (!_hasScore) return null;
return value == null || value.trim().isEmpty return value == null || value.trim().isEmpty
@ -431,10 +421,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
TextFormField( TextFormField(
controller: _scoreUnitController, controller: _scoreUnitController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Unité'),
labelText: 'Unité',
border: OutlineInputBorder(),
),
validator: (value) { validator: (value) {
if (!_hasScore) return null; if (!_hasScore) return null;
return value == null || value.trim().isEmpty return value == null || value.trim().isEmpty
@ -684,10 +671,7 @@ final class _MediaImportField extends StatelessWidget {
: 'Aucun fichier sélectionné' : 'Aucun fichier sélectionné'
: fileName; : fileName;
return InputDecorator( return InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(labelText: label),
labelText: label,
border: const OutlineInputBorder(),
),
child: Row( child: Row(
children: [ children: [
Icon(imported ? Icons.check_circle_outline : Icons.perm_media), Icon(imported ? Icons.check_circle_outline : Icons.perm_media),

View File

@ -2,21 +2,32 @@ import 'package:flutter/material.dart';
import '../application/app_bootstrap.dart'; import '../application/app_bootstrap.dart';
import 'home_screen.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}); const GameTimeApp({required this.bootstrap, super.key});
final AppBootstrap bootstrap; final AppBootstrap bootstrap;
@override
State<GameTimeApp> createState() => _GameTimeAppState();
}
final class _GameTimeAppState extends State<GameTimeApp> {
var _themeMode = ThemeMode.system;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'GameTime', title: 'GameTime',
theme: ThemeData( theme: courtBlazerLightTheme(),
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0E7C66)), darkTheme: courtBlazerDarkTheme(),
useMaterial3: true, themeMode: _themeMode,
home: HomeScreen(
bootstrap: widget.bootstrap,
themeMode: _themeMode,
onThemeModeChanged: (mode) => setState(() => _themeMode = mode),
), ),
home: HomeScreen(bootstrap: bootstrap),
); );
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../application/application.dart'; import '../application/application.dart';
import '../domain/domain.dart'; import '../domain/domain.dart';
import 'theme.dart';
import 'workout_execution_screen.dart'; import 'workout_execution_screen.dart';
final class HistoryListScreen extends StatefulWidget { final class HistoryListScreen extends StatefulWidget {
@ -62,7 +63,9 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
), ),
), ),
for (final history in group.value) for (final history in group.value)
ListTile( CourtBlazerAccentPanel(
margin: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: ListTile(
title: Text(history.nameSnapshot), title: Text(history.nameSnapshot),
subtitle: Text( subtitle: Text(
'${_formatDateTime(history.startedAt)} · ' '${_formatDateTime(history.startedAt)} · '
@ -72,6 +75,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _openDetail(history), onTap: () => _openDetail(history),
), ),
),
], ],
], ],
); );

View File

@ -5,13 +5,21 @@ import '../domain/domain.dart';
import 'exercise_library_screen.dart'; import 'exercise_library_screen.dart';
import 'history_screen.dart'; import 'history_screen.dart';
import 'program_screen.dart'; import 'program_screen.dart';
import 'theme.dart';
import 'workout_execution_screen.dart'; import 'workout_execution_screen.dart';
import 'workout_template_screen.dart'; import 'workout_template_screen.dart';
final class HomeScreen extends StatefulWidget { 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 AppBootstrap bootstrap;
final ThemeMode themeMode;
final ValueChanged<ThemeMode>? onThemeModeChanged;
@override @override
State<HomeScreen> createState() => _HomeScreenState(); State<HomeScreen> createState() => _HomeScreenState();
@ -29,7 +37,22 @@ final class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('GameTime')), appBar: AppBar(
title: const GameTimeLogo(),
actions: [
PopupMenuButton<ThemeMode>(
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( body: ListView(
children: [ children: [
FutureBuilder<ActiveWorkoutSession?>( FutureBuilder<ActiveWorkoutSession?>(
@ -46,7 +69,9 @@ final class _HomeScreenState extends State<HomeScreen> {
setIndex: session.currentSetIndex, setIndex: session.currentSetIndex,
); );
final exercise = plan.exerciseAt(position); final exercise = plan.exerciseAt(position);
return MaterialBanner( return CourtBlazerAccentPanel(
margin: const EdgeInsets.all(12),
child: MaterialBanner(
content: Text( content: Text(
'Séance en cours — Reprendre · ${exercise.name}, ' 'Séance en cours — Reprendre · ${exercise.name}, '
'série ${position.setIndex + 1}/${exercise.setsCount}', 'série ${position.setIndex + 1}/${exercise.setsCount}',
@ -57,6 +82,7 @@ final class _HomeScreenState extends State<HomeScreen> {
child: const Text('Reprendre'), child: const Text('Reprendre'),
), ),
], ],
),
); );
}, },
), ),

View File

@ -3,5 +3,6 @@ export 'exercise_library_screen.dart';
export 'history_screen.dart'; export 'history_screen.dart';
export 'home_screen.dart'; export 'home_screen.dart';
export 'program_screen.dart'; export 'program_screen.dart';
export 'theme.dart';
export 'workout_execution_screen.dart'; export 'workout_execution_screen.dart';
export 'workout_template_screen.dart'; export 'workout_template_screen.dart';

View File

@ -181,10 +181,7 @@ final class _ProgramFormScreenState extends State<ProgramFormScreen> {
children: [ children: [
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Nom'),
labelText: 'Nom',
border: OutlineInputBorder(),
),
validator: (value) => value == null || value.trim().isEmpty validator: (value) => value == null || value.trim().isEmpty
? 'Le nom est obligatoire.' ? 'Le nom est obligatoire.'
: null, : null,
@ -194,7 +191,6 @@ final class _ProgramFormScreenState extends State<ProgramFormScreen> {
controller: _defaultRestController, controller: _defaultRestController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Repos par défaut (s)', labelText: 'Repos par défaut (s)',
border: OutlineInputBorder(),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: _nonNegativeIntValidator, validator: _nonNegativeIntValidator,
@ -356,7 +352,6 @@ final class _ExerciseSelectionScreenState
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Rechercher', labelText: 'Rechercher',
prefixIcon: Icon(Icons.search), prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -467,10 +462,7 @@ final class _ProgramExerciseCard extends StatelessWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
TextFormField( TextFormField(
initialValue: '${draft.setsCount}', initialValue: '${draft.setsCount}',
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Nombre de séries'),
labelText: 'Nombre de séries',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) { onChanged: (value) {
draft.setsCount = int.tryParse(value) ?? draft.setsCount; draft.setsCount = int.tryParse(value) ?? draft.setsCount;
@ -530,7 +522,6 @@ final class _ProgramExerciseCard extends StatelessWidget {
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Repos après chaque série (s)', labelText: 'Repos après chaque série (s)',
helperText: 'Pas de repos après la toute dernière série.', helperText: 'Pas de repos après la toute dernière série.',
border: OutlineInputBorder(),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) { onChanged: (value) {
@ -571,10 +562,7 @@ final class _NumberField extends StatelessWidget {
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: TextFormField( child: TextFormField(
initialValue: initialValue?.toString(), initialValue: initialValue?.toString(),
decoration: InputDecoration( decoration: InputDecoration(labelText: label),
labelText: label,
border: const OutlineInputBorder(),
),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) => onChanged(double.tryParse(value)), onChanged: (value) => onChanged(double.tryParse(value)),
), ),

436
lib/presentation/theme.dart Normal file
View File

@ -0,0 +1,436 @@
import 'package:flutter/material.dart';
const _radius = 6.0;
const _badgeRadius = 5.0;
@immutable
final class CourtBlazerTokens extends ThemeExtension<CourtBlazerTokens> {
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<CourtBlazerTokens>? 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>() ??
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>() ??
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;
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import '../application/application.dart'; import '../application/application.dart';
import '../domain/domain.dart'; import '../domain/domain.dart';
import 'history_screen.dart'; import 'history_screen.dart';
import 'theme.dart';
enum WorkoutExecutionMode { active, rest, paused, finished } enum WorkoutExecutionMode { active, rest, paused, finished }
@ -146,7 +147,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
Center( Center(
child: Text( child: Text(
_formatDuration(Duration(seconds: _remainingRestSeconds)), _formatDuration(Duration(seconds: _remainingRestSeconds)),
style: Theme.of(context).textTheme.displayLarge, style: AppTextStyles.timer(context),
), ),
), ),
if (next != null && nextExercise != null) ...[ if (next != null && nextExercise != null) ...[
@ -533,7 +534,7 @@ final class SetMeasureInput extends StatelessWidget {
exercise.targetTimeSeconds == null exercise.targetTimeSeconds == null
? 'Chronométrer' ? 'Chronométrer'
: '${exercise.targetTimeSeconds} s', : '${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()), onRepsChanged((reps - 1).clamp(0, 999).toInt()),
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
), ),
Text('$reps'), Text('$reps', style: AppTextStyles.scoreNumber(context)),
IconButton( IconButton(
onPressed: () => onRepsChanged(reps + 1), onPressed: () => onRepsChanged(reps + 1),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
@ -561,11 +562,11 @@ final class SetMeasureInput extends StatelessWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
TextField( TextField(
controller: scoreController, controller: scoreController,
style: AppTextStyles.scoreNumber(context),
decoration: InputDecoration( decoration: InputDecoration(
labelText: exercise.scoreUnit == null labelText: exercise.scoreUnit == null
? 'Score' ? 'Score'
: 'Score (${exercise.scoreUnit})', : 'Score (${exercise.scoreUnit})',
border: const OutlineInputBorder(),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
@ -770,13 +771,16 @@ final class _Header extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return CourtBlazerAccentPanel(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(elapsedLabel, style: Theme.of(context).textTheme.headlineSmall), Text(elapsedLabel, style: AppTextStyles.scoreNumber(context)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(progressLabel), Text(progressLabel),
], ],
),
); );
} }
} }

View File

@ -205,10 +205,7 @@ final class _WorkoutTemplateFormScreenState
children: [ children: [
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: 'Nom'),
labelText: 'Nom',
border: OutlineInputBorder(),
),
validator: (value) => value == null || value.trim().isEmpty validator: (value) => value == null || value.trim().isEmpty
? 'Le nom est obligatoire.' ? 'Le nom est obligatoire.'
: null, : null,
@ -420,7 +417,6 @@ final class _WorkoutTemplateProgramDetailScreenState
'${exercise.setsCountOverride ?? exercise.setsCount}', '${exercise.setsCountOverride ?? exercise.setsCount}',
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Nombre de séries', labelText: 'Nombre de séries',
border: OutlineInputBorder(),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) { onChanged: (value) {
@ -633,10 +629,7 @@ final class _NumberField extends StatelessWidget {
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: TextFormField( child: TextFormField(
initialValue: initialValue?.toString(), initialValue: initialValue?.toString(),
decoration: InputDecoration( decoration: InputDecoration(labelText: label),
labelText: label,
border: const OutlineInputBorder(),
),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (value) => onChanged(double.tryParse(value)), onChanged: (value) => onChanged(double.tryParse(value)),
), ),

View File

@ -27,3 +27,10 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
fonts:
- family: Anton
fonts:
- asset: assets/fonts/Anton-Regular.ttf
- family: Archivo
fonts:
- asset: assets/fonts/Archivo-Variable.ttf