179 lines
5.6 KiB
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,
|
|
);
|