feat(server): implemente synchronisation et serveur avec fixtures (#187)
- Implémente la couche de synchronisation avec le serveur - Ajoute les fixtures versionnées pour les tests - Met à jour Drift database et repositories pour le support sync - Améliore les tests de synchronisation - Corrige et améliore le watch companion pour la collecte de métriques Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -43,6 +43,7 @@ final class ExerciseLibraryScreen extends StatefulWidget {
|
||||
|
||||
final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
final _searchController = TextEditingController();
|
||||
var _selectedBusinessTypes = <BusinessExerciseType?>{};
|
||||
var _selectedMeasures = <WorkoutMeasure>{};
|
||||
var _selectedTags = <String>{};
|
||||
late Future<List<Exercise>> _exercises;
|
||||
@ -84,6 +85,7 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
}
|
||||
|
||||
final exercises = snapshot.data ?? const <Exercise>[];
|
||||
final typeFilterOptions = _typeFilterOptionsFor(exercises);
|
||||
final tagSuggestions = tagSuggestionsFor(
|
||||
exercises,
|
||||
(exercise) => exercise.tags,
|
||||
@ -110,6 +112,41 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (typeFilterOptions.length > 1) ...[
|
||||
Text(
|
||||
'Type',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final type in typeFilterOptions)
|
||||
FilterChip(
|
||||
label: Text(_businessTypeFilterLabel(type)),
|
||||
selected: _selectedBusinessTypes.contains(
|
||||
type,
|
||||
),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedBusinessTypes = {
|
||||
..._selectedBusinessTypes,
|
||||
type,
|
||||
};
|
||||
} else {
|
||||
_selectedBusinessTypes = {
|
||||
..._selectedBusinessTypes,
|
||||
}..remove(type);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
@ -169,7 +206,7 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
child: _CenteredMessage(
|
||||
title: 'Aucun exercice ne correspond',
|
||||
message:
|
||||
'Modifie la recherche, les mesures ou les tags sélectionnés.',
|
||||
'Modifie la recherche, les types, les mesures ou les tags sélectionnés.',
|
||||
actionLabel: 'Effacer les filtres',
|
||||
onAction: _clearFilters,
|
||||
),
|
||||
@ -206,7 +243,15 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
final matchesMeasures =
|
||||
_selectedMeasures.isEmpty ||
|
||||
_selectedMeasures.every(exercise.availableMeasures.contains);
|
||||
return matchesQuery && matchesMeasures;
|
||||
final matchesTypes =
|
||||
_selectedBusinessTypes.isEmpty ||
|
||||
_selectedBusinessTypes.any((type) {
|
||||
final effectiveTypes = exercise.effectiveBusinessTypes;
|
||||
return type == null
|
||||
? effectiveTypes.isEmpty
|
||||
: effectiveTypes.contains(type);
|
||||
});
|
||||
return matchesQuery && matchesTypes && matchesMeasures;
|
||||
}).toList();
|
||||
return filterByRequiredTags(
|
||||
matchingQueryAndMeasures,
|
||||
@ -217,6 +262,7 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
|
||||
bool get _filtersActive {
|
||||
return _searchController.text.trim().isNotEmpty ||
|
||||
_selectedBusinessTypes.isNotEmpty ||
|
||||
_selectedMeasures.isNotEmpty ||
|
||||
_selectedTags.isNotEmpty;
|
||||
}
|
||||
@ -234,6 +280,7 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
void _clearFilters() {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
_selectedBusinessTypes = {};
|
||||
_selectedMeasures = {};
|
||||
_selectedTags = {};
|
||||
});
|
||||
@ -314,6 +361,27 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
List<BusinessExerciseType?> _typeFilterOptionsFor(List<Exercise> exercises) {
|
||||
final options = <BusinessExerciseType?>[
|
||||
for (final type in BusinessExerciseType.values)
|
||||
if (exercises.any((exercise) {
|
||||
return exercise.effectiveBusinessTypes.contains(type);
|
||||
}))
|
||||
type,
|
||||
];
|
||||
final hasUntypedExercise = exercises.any(
|
||||
(exercise) => exercise.effectiveBusinessTypes.isEmpty,
|
||||
);
|
||||
if (hasUntypedExercise) {
|
||||
options.add(null);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
String _businessTypeFilterLabel(BusinessExerciseType? type) {
|
||||
return type == null ? 'Libre / autre' : type.label;
|
||||
}
|
||||
|
||||
final class ExerciseListTile extends StatelessWidget {
|
||||
const ExerciseListTile({
|
||||
required this.exercise,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2332,7 +2332,7 @@ final class _LiveSensorBar extends StatelessWidget {
|
||||
_LiveSensorPill(
|
||||
icon: Icons.directions_run,
|
||||
label: _distanceMetricLabel(sensorState),
|
||||
muted: _distanceLabel(sensorState) == null,
|
||||
muted: !_isDistanceAvailable(sensorState),
|
||||
),
|
||||
_LiveSensorPill(
|
||||
icon: Icons.local_fire_department,
|
||||
@ -5332,6 +5332,9 @@ String _heartRateMetricLabel(ActiveWorkoutSensorState? state) {
|
||||
}
|
||||
|
||||
String _distanceMetricLabel(ActiveWorkoutSensorState? state) {
|
||||
if (!_isDistanceAvailable(state)) {
|
||||
return _sensorUnavailableLabel(state);
|
||||
}
|
||||
return _distanceLabel(state) ?? _sensorUnavailableLabel(state);
|
||||
}
|
||||
|
||||
@ -5357,6 +5360,12 @@ String? _distanceLabel(ActiveWorkoutSensorState? state) {
|
||||
return '${meters.round()} m';
|
||||
}
|
||||
|
||||
bool _isDistanceAvailable(ActiveWorkoutSensorState? state) {
|
||||
return state != null &&
|
||||
state.latestDistanceAvailable &&
|
||||
_distanceLabel(state) != null;
|
||||
}
|
||||
|
||||
String? _caloriesLabel(ActiveWorkoutSensorState? state) {
|
||||
final actualCalories = state?.latestCaloriesKcal;
|
||||
if (actualCalories != null && actualCalories >= 0) {
|
||||
|
||||
Reference in New Issue
Block a user