- Notifications
You must be signed in to change notification settings - Fork 0
Draft design document for Add Custom Exercise feature#14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Devasy
merged 8 commits into
main
from
docs-add-custom-exercise-feature-9646738705101450413Jan 26, 2026
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
02ec114
Draft design document for Adding Custom Exercises
google-labs-jules[bot] 9f7e1e7
feat: Add core workout logging functionality including exercise libra…
Devasy df4f2c3
Adds new feature for editing and fixes some review comments
Devasy 64c7126
feat: Implement core workout tracking features including custom exerc…
Devasy 9af7105
feat: Introduce workout session editing, history screen, workout prov…
Devasy 07f9194
feat: Add screen for editing workout sessions and a workout provider.
Devasy e60a4a8
feat: Add workout session editing, history screen, workout provider, …
Devasy 363cf77
Update workout-logger/lib/screens/edit_workout_session_screen.dart
Devasy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff 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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff 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 { | ||
| @@ -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; | ||
| } | ||
| @@ -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?, | ||
| ); | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| class DropsetEntry { | ||
| @@ -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); | ||
| @@ -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 ==================== | ||
| @@ -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 ==================== | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.