Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/design/add_custom_exercise.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
# Design Document: Personalized Exercise Library Feature

## 1. Overview
This document outlines the design and implementation plan for adding a "Personalized Exercise" feature to the Workout Logger application. This feature allows users to create and add their own custom exercises to the library, expanding beyond the built-in database.

## 2. Feature Requirements

### 2.1 User Stories
- **As a user**, I want to add a new exercise that is not in the default list, so I can track my specific workout routines.
- **As a user**, I want to specify the name, category (Compound/Isolation), and primary muscle group for my custom exercise.
- **As a user**, I want to see my custom exercises integrated seamlessly with the built-in exercises in the library.
- **As a user**, I want to be able to delete custom exercises I no longer need (Optional for v1, but good to consider).

### 2.2 Functional Requirements
- **Add Exercise Form**: A dedicated screen for inputting exercise details.
- **Validation**: Ensure exercise name is not empty and muscle group/category are selected.
- **Persistence**: Save custom exercises locally using the existing Hive-based storage.
- **Integration**: Display custom exercises in the `ExerciseLibraryScreen` alongside built-in ones.

## 3. Technical Architecture

### 3.1 Data Layer (`lib/data`, `lib/models`)
- **Model**: The existing `Exercise` model is sufficient. It already has an `isCustom` flag.
```dart
class Exercise {
final String id;
final String name;
final List<MuscleActivation> muscleActivations; // Derived from selected muscles
final String category; // 'compound' or 'isolation'
final bool isCustom; // Set to true for new exercises
// ...
}
```
- **Storage**: `StorageService` (`lib/services/storage_service.dart`) already implements methods for custom exercises (`saveCustomExercise`, `getCustomExercises`, `deleteCustomExercise`). No changes required here.

### 3.2 State Management (`lib/services/workout_provider.dart`)
The `WorkoutProvider` manages the app state. We need to expose a method to add a custom exercise.

**Proposed Changes:**
- Add `addCustomExercise(String name, String category, String muscleGroupId)` method.
- Generate a unique ID (using `Uuid`).
- Create `Exercise` object with `isCustom: true`.
- Create `MuscleActivation` list (Simplified for v1: 100% activation for the selected primary muscle).
- Call `_storage.saveCustomExercise()`.
- Add to `_allExercises` list.
- `notifyListeners()` to update UI.

### 3.3 UI Layer (`lib/screens`)

#### A. `ExerciseLibraryScreen` Update
- **Data Source**: Change `ExerciseDatabase.getAll()` to `context.watch<WorkoutProvider>().allExercises`. This ensures the list updates when a new exercise is added.
- **Action**: Add a `FloatingActionButton` (or an action button in AppBar) to navigate to the new `AddCustomExerciseScreen`.

#### B. New Screen: `AddCustomExerciseScreen`
- **Widgets**:
- `TextFormField` for Exercise Name.
- `DropdownButtonFormField` or `SegmentedButton` for Category (Compound/Isolation).
- `DropdownButtonFormField` for Primary Muscle Group (using `MuscleGroups.names`).
- `ElevatedButton` for "Save Exercise".
- **Validation**: Use `GlobalKey<FormState>` to validate inputs before submission.
- **Feedback**: Show a `SnackBar` (Toast) upon successful creation or error.

## 4. Implementation Steps

1. **Update `WorkoutProvider`**:
- Implement `addCustomExercise` method.
- Ensure `_allExercises` is correctly populated on `init()` by merging built-in and custom exercises (already partially implemented in `loadAllData` calling `_storage.getAllExercises`).

2. **Create `AddCustomExerciseScreen`**:
- Create `lib/screens/add_custom_exercise_screen.dart`.
- Implement the form with validation.
- Connect to `WorkoutProvider`.

3. **Update `ExerciseLibraryScreen`**:
- Replace static data fetch with Provider listener.
- Add navigation to `AddCustomExerciseScreen`.

## 5. Flutter Best Practices Adherence

- **State Management**: Use `Provider` for business logic and state. UI components should only react to state changes.
- **Immutability**: Ensure `Exercise` objects are immutable. Use `List.from()` when modifying lists to avoid reference issues.
- **Asynchronous Operations**: Handle storage operations asynchronously. Show loading indicators if necessary (though strictly local storage is fast).
- **Form Validation**: Use standard Flutter `Form` and `TextFormField` validation logic.
- **Theming**: Use `AppTheme` constants (colors, spacing, typography) to maintain consistency with the "Samsung Health-style" minimal interface.
- **User Feedback**: Provide immediate feedback (SnackBar) for user actions.

## 6. Testing Strategy

- **Unit Tests**:
- Test `WorkoutProvider.addCustomExercise`:
- Verify exercise is added to the list.
- Verify `saveCustomExercise` is called on storage.
- Verify `isCustom` flag is set.
- **Widget Tests**:
- Test `AddCustomExerciseScreen`:
- Verify validation errors show up for empty name.
- Verify submitting form calls the provider method.
- Test `ExerciseLibraryScreen`:
- Verify custom exercises appear in the list.

## 7. Future Considerations (v2)
- **Advanced Muscle Activation**: Allow users to select multiple muscle groups and specify activation percentages (e.g., Chest 70%, Triceps 30%).
- **Icon Selection**: Allow users to pick an icon for their custom exercise.
- **Edit/Delete**: Implement functionality to edit or remove custom exercises.
Comment thread
Devasy marked this conversation as resolved.
3 changes: 3 additions & 0 deletions workout-logger/devtools_options.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
60 changes: 53 additions & 7 deletions workout-logger/lib/models/models.dart
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
// Data Models for Workout Logger App

// Sentinel value for copyWith methods to distinguish "not provided" from "null"
const Object _sentinel = Object();

// ==================== Muscle Groups ====================

class MuscleGroup {
Expand DownExpand Up@@ -72,8 +75,9 @@ class Exercise {

String get primaryMuscle {
if (muscleActivations.isEmpty) return 'Unknown';
final sorted = List<MuscleActivation>.from(muscleActivations)
..sort((a, b) => b.activationPercentage.compareTo(a.activationPercentage));
final sorted = List<MuscleActivation>.from(
muscleActivations,
)..sort((a, b) => b.activationPercentage.compareTo(a.activationPercentage));
return sorted.first.muscleGroupId;
}

Expand DownExpand Up@@ -144,6 +148,22 @@ class WorkoutSet {
timeTaken: json['timeTaken'],
timestamp: DateTime.parse(json['timestamp']),
);

WorkoutSet copyWith({
Object? weight = _sentinel,
Object? reps = _sentinel,
Object? isDropset = _sentinel,
Object? drops = _sentinel,
Object? timeTaken = _sentinel,
Object? timestamp = _sentinel,
}) => WorkoutSet(
weight: weight == _sentinel ? this.weight : weight as double,
reps: reps == _sentinel ? this.reps : reps as int,
isDropset: isDropset == _sentinel ? this.isDropset : isDropset as bool,
drops: drops == _sentinel ? this.drops : drops as List<DropsetEntry>?,
timeTaken: timeTaken == _sentinel ? this.timeTaken : timeTaken as int?,
timestamp: timestamp == _sentinel ? this.timestamp : timestamp as DateTime?,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

class DropsetEntry {
Expand All@@ -167,11 +187,7 @@ class ExerciseLog {
final List<WorkoutSet> sets;
final String? notes;

ExerciseLog({
required this.exerciseId,
required this.sets,
this.notes,
});
ExerciseLog({required this.exerciseId, required this.sets, this.notes});

double get totalVolume => sets.fold(0.0, (sum, set) => sum + set.volume);

Expand All@@ -186,6 +202,18 @@ class ExerciseLog {
sets: (json['sets'] as List).map((s) => WorkoutSet.fromJson(s)).toList(),
notes: json['notes'],
);

ExerciseLog copyWith({
Object? exerciseId = _sentinel,
Object? sets = _sentinel,
Object? notes = _sentinel,
}) => ExerciseLog(
exerciseId: exerciseId == _sentinel
? this.exerciseId
: exerciseId as String,
sets: sets == _sentinel ? this.sets : sets as List<WorkoutSet>,
notes: notes == _sentinel ? this.notes : notes as String?,
);
}

// ==================== Workout Session ====================
Expand DownExpand Up@@ -229,6 +257,24 @@ class WorkoutSession {
duration: json['duration'],
notes: json['notes'],
);

WorkoutSession copyWith({
Object? id = _sentinel,
Object? date = _sentinel,
Object? routineId = _sentinel,
Object? exercises = _sentinel,
Object? duration = _sentinel,
Object? notes = _sentinel,
}) => WorkoutSession(
id: id == _sentinel ? this.id : id as String,
date: date == _sentinel ? this.date : date as DateTime,
routineId: routineId == _sentinel ? this.routineId : routineId as String?,
exercises: exercises == _sentinel
? this.exercises
: exercises as List<ExerciseLog>,
duration: duration == _sentinel ? this.duration : duration as int,
notes: notes == _sentinel ? this.notes : notes as String?,
);
}

// ==================== Routine ====================
Expand Down
Loading