glp-companion/lib/features/exercise/presentation/controllers/exercise_controller.dart

179 lines
5.6 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/utils/date_time_utils.dart';
import '../../../../core/utils/uuid.dart';
import '../../data/repositories/in_memory_exercise_repository.dart';
import '../../domain/entities/exercise_entry.dart';
import '../../domain/entities/muscle_group.dart';
import '../../domain/repositories/exercise_repository.dart';
/// Application-wide exercise repository.
///
/// Overridable in widget/unit tests with a fake implementation.
final exerciseRepositoryProvider = Provider<IExerciseRepository>(
(ref) => InMemoryExerciseRepository(),
);
/// Immutable snapshot of the resistance-training log.
class ExerciseState {
const ExerciseState({
required this.entries,
required this.weekStart,
this.errorMessage,
});
/// All logged entries, newest first.
final List<ExerciseEntry> entries;
/// First day (Monday) of the current ISO week at local start-of-day.
final DateTime weekStart;
/// Last persistence failure message, cleared on the next success.
final String? errorMessage;
/// The seven days of the current window, Monday through Sunday.
List<DateTime> get weekDays => List<DateTime>.unmodifiable(
List.generate(
7,
(index) => DateTime(
weekStart.year,
weekStart.month,
weekStart.day + index,
),
),
);
/// Entries logged inside the current ISO week, newest first.
List<ExerciseEntry> get entriesThisWeek => entries
.where(
(entry) =>
!entry.timestamp.isBefore(weekStart) &&
entry.timestamp.isBefore(weekStart.add(const Duration(days: 7))),
)
.toList(growable: false);
/// Number of distinct calendar days with at least one logged session in
/// the current week — the weekly strength-workout frequency used for the
/// lean-mass-preservation card.
int get weeklyWorkoutCount {
final days = <DateTime>{};
for (final entry in entriesThisWeek) {
days.add(startOfDay(entry.timestamp));
}
return days.length;
}
ExerciseState copyWith({
List<ExerciseEntry>? entries,
DateTime? weekStart,
Object? errorMessage = _keep,
}) =>
ExerciseState(
entries: entries ?? this.entries,
weekStart: weekStart ?? this.weekStart,
errorMessage: identical(errorMessage, _keep)
? this.errorMessage
: errorMessage as String?,
);
/// Sentinel allowing `copyWith` to explicitly reset a field to `null`.
static const Object _keep = Object();
}
/// Orchestrates the resistance-training log: persists working sets, derives
/// the weekly strength-session frequency (target: 3 sessions/week for lean
/// mass preservation during GLP-1 therapy), and exposes delete/error flows.
class ExerciseController extends Notifier<ExerciseState> {
/// Recommended weekly strength-session frequency for lean mass
/// preservation on GLP-1/GIP agonist therapy.
static const int recommendedWeeklySessions = 3;
@override
ExerciseState build() => ExerciseState(
entries: const [],
weekStart: startOfWeek(DateTime.now()),
);
/// Persists a working set and refreshes the snapshot.
///
/// Returns whether the write succeeded.
Future<bool> logExercise({
required MuscleGroup muscleGroup,
required String exerciseName,
required int sets,
required int reps,
double weightLbs = 0,
DateTime? timestamp,
}) async {
final trimmed = exerciseName.trim();
if (trimmed.isEmpty || sets <= 0 || reps <= 0 || weightLbs < 0) {
state = state.copyWith(errorMessage: 'Check the entered values.');
return false;
}
try {
await ref.read(exerciseRepositoryProvider).saveEntry(
ExerciseEntry(
id: generateUuid(),
timestamp: timestamp ?? DateTime.now(),
muscleGroup: muscleGroup,
exerciseName: trimmed,
sets: sets,
reps: reps,
weightLbs: weightLbs,
),
);
await _refresh();
return true;
} catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: 'saveEntry failed');
state = state.copyWith(errorMessage: 'Could not log the exercise.');
return false;
}
}
/// Removes an entry by id.
Future<bool> deleteEntry(String id) async {
try {
await ref.read(exerciseRepositoryProvider).deleteEntry(id);
await _refresh();
return true;
} catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: 'deleteEntry failed');
state = state.copyWith(errorMessage: 'Could not delete the entry.');
return false;
}
}
/// Clears the transient error banner.
void clearError() {
if (state.errorMessage != null) {
state = state.copyWith(errorMessage: null);
}
}
/// Reloads the full log, rolling the week window forward when the ISO week
/// changed since the last snapshot.
Future<void> _refresh() async {
try {
final entries =
await ref.read(exerciseRepositoryProvider).getEntries();
state = state.copyWith(
entries: entries,
weekStart: startOfWeek(DateTime.now()),
errorMessage: null,
);
} catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: 'exercise refresh failed');
state = state.copyWith(errorMessage: 'Could not load the exercise log.');
}
}
}
/// Exposes the [ExerciseController] to the widget tree.
final exerciseControllerProvider =
NotifierProvider<ExerciseController, ExerciseState>(
ExerciseController.new,
);