Add remote exercise library fetching from GitHub - #34
Conversation
Adds a mechanism to fetch and cache exercises from a JSON file hosted in this repo's main branch (exercises.json). Users tap the cloud-download button in Exercise Library to fetch 10 new exercises; they persist in a dedicated Hive box (remote_exercises) across restarts and appear with a cyan REMOTE badge. Key changes: - exercises.json: 10 curated exercises (cable crossover, meadows row, z-press, etc.) - IStorageService: 3 new methods (saveRemoteExercises, getRemoteExercises, clearRemoteExercises) - StorageService: remote_exercises Hive box, deduplication in getAllExercises() - WorkoutProvider: fetchRemoteExercises() with loading/error state, http + timeout - ExerciseLibraryScreen: cloud-download AppBar button, REMOTE badge on cards - MockStorageService: implements the 3 new interface methods https://claude.ai/code/session_01U3WJ7yBDYzNbYpo7LyDfzW
WalkthroughIntroduces remote exercise support by adding a predefined exercises.json file, implementing HTTP fetching of remote exercises to persistent storage via a new Hive box, extending the storage service interface and implementation with remote exercise management methods, adding fetch state tracking to the provider, and updating the exercise library UI to display and filter remote exercises alongside built-in and custom exercises. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
workout-logger/lib/services/storage_service.dart (1)
381-413: 🧹 Nitpick | 🔵 TrivialNote: Remote exercises are intentionally excluded from export.
The
exportAllData()method doesn't include remote exercises, which is correct since they can be re-fetched. If this is intentional, consider adding a brief comment to document this design decision for future maintainers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@workout-logger/lib/services/storage_service.dart` around lines 381 - 413, The exportAllData() method currently omits remote exercises intentionally but lacks documentation; update the exportAllData implementation (around the export map construction in exportAllData) to include a short explanatory comment that "remote exercises intentionally excluded — they are fetched from remote sources and should not be exported" (or similar) so future maintainers understand the design decision; reference exportAllData, _customExercisesBoxInstance, and the data map keys (e.g., 'customExercises') when adding the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@workout-logger/lib/screens/exercise_library_screen.dart`:
- Around line 62-63: The file repeatedly checks e.id.startsWith('remote_') (used
in the rank(Exercise e) lambda and 8+ other places); add a single source of
truth by adding a boolean getter on the Exercise model (e.g., Exercise.get
isRemote) that encapsulates id.startsWith('remote_'), then replace all
occurrences (including the rank function and other checks) with e.isRemote; this
keeps the id convention centralized and makes future changes trivial.
In `@workout-logger/lib/services/storage_service.dart`:
- Around line 311-319: The saveRemoteExercises method currently clears
_remoteExercisesBoxInstance then loops calling put per exercise which is
inefficient for large datasets; replace the per-item puts with a single batch
write by building a Map keyed by exercise.id with values
jsonEncode(exercise.toJson()) and call _remoteExercisesBoxInstance.putAll(...)
after the clear to perform one bulk write (keep the clear and the jsonEncode
logic and function name saveRemoteExercises unchanged).
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 299-303: The parsing of the remote response assumes
decoded['exercises'] exists and is a List, causing a type cast exception on
malformed JSON; update the code around decoded, exerciseList and the mapping to
first check that decoded is a Map<String, dynamic>, that
decoded.containsKey('exercises') and that decoded['exercises'] is a List (not
null), and if not either return an empty exercises list or throw a clear,
specific exception / log a descriptive error before the generic catch; then
safely cast each item and use Exercise.fromJson only after verifying each item
is a Map<String, dynamic> to avoid runtime cast errors.
---
Outside diff comments:
In `@workout-logger/lib/services/storage_service.dart`:
- Around line 381-413: The exportAllData() method currently omits remote
exercises intentionally but lacks documentation; update the exportAllData
implementation (around the export map construction in exportAllData) to include
a short explanatory comment that "remote exercises intentionally excluded — they
are fetched from remote sources and should not be exported" (or similar) so
future maintainers understand the design decision; reference exportAllData,
_customExercisesBoxInstance, and the data map keys (e.g., 'customExercises')
when adding the comment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 406d527e-c086-449e-802b-8e8d0115472b
📒 Files selected for processing (6)
exercises.jsonworkout-logger/lib/screens/exercise_library_screen.dartworkout-logger/lib/services/interfaces/storage_service_interface.dartworkout-logger/lib/services/storage_service.dartworkout-logger/lib/services/workout_provider.dartworkout-logger/test/test_utils/mock_storage_service.dart
| int rank(Exercise e) => | ||
| e.isCustom ? 0 : e.id.startsWith('remote_') ? 1 : 2; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting remote exercise identification to reduce duplication.
The e.id.startsWith('remote_') check appears 8+ times across this file. Extracting this to a helper or adding an isRemote getter to the Exercise model would improve maintainability and make the ID convention easier to change in the future.
♻️ Option 1: Local helper function
// Add at top of file or in a utils file
bool isRemoteExercise(Exercise e) => e.id.startsWith('remote_');
// Then use throughout:
int rank(Exercise e) =>
e.isCustom ? 0 : isRemoteExercise(e) ? 1 : 2;♻️ Option 2: Add getter to Exercise model (preferred)
// In models.dart, add to Exercise class:
bool get isRemote => id.startsWith('remote_');
// Then use throughout:
int rank(Exercise e) =>
e.isCustom ? 0 : e.isRemote ? 1 : 2;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@workout-logger/lib/screens/exercise_library_screen.dart` around lines 62 -
63, The file repeatedly checks e.id.startsWith('remote_') (used in the
rank(Exercise e) lambda and 8+ other places); add a single source of truth by
adding a boolean getter on the Exercise model (e.g., Exercise.get isRemote) that
encapsulates id.startsWith('remote_'), then replace all occurrences (including
the rank function and other checks) with e.isRemote; this keeps the id
convention centralized and makes future changes trivial.
| Future<void> saveRemoteExercises(List<Exercise> exercises) async { | ||
| await _remoteExercisesBoxInstance.clear(); | ||
| for (final exercise in exercises) { | ||
| await _remoteExercisesBoxInstance.put( | ||
| exercise.id, | ||
| jsonEncode(exercise.toJson()), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using putAll for better performance with larger datasets.
The current implementation iterates and calls put for each exercise. While fine for the current 10 exercises, if the remote library grows, putAll would be more efficient.
💡 Batch write using putAll
`@override`
Future<void> saveRemoteExercises(List<Exercise> exercises) async {
await _remoteExercisesBoxInstance.clear();
- for (final exercise in exercises) {
- await _remoteExercisesBoxInstance.put(
- exercise.id,
- jsonEncode(exercise.toJson()),
- );
- }
+ final entries = {
+ for (final exercise in exercises)
+ exercise.id: jsonEncode(exercise.toJson()),
+ };
+ await _remoteExercisesBoxInstance.putAll(entries);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@workout-logger/lib/services/storage_service.dart` around lines 311 - 319, The
saveRemoteExercises method currently clears _remoteExercisesBoxInstance then
loops calling put per exercise which is inefficient for large datasets; replace
the per-item puts with a single batch write by building a Map keyed by
exercise.id with values jsonEncode(exercise.toJson()) and call
_remoteExercisesBoxInstance.putAll(...) after the clear to perform one bulk
write (keep the clear and the jsonEncode logic and function name
saveRemoteExercises unchanged).
| final decoded = jsonDecode(response.body) as Map<String, dynamic>; | ||
| final exerciseList = decoded['exercises'] as List<dynamic>; | ||
| final exercises = exerciseList | ||
| .map((e) => Exercise.fromJson(e as Map<String, dynamic>)) | ||
| .toList(); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider defensive checks for malformed JSON responses.
If the remote JSON is malformed (missing exercises key or null value), the as List<dynamic> cast will throw, which is caught by the generic catch. However, the error message will be a type cast exception rather than something actionable.
💡 Suggested defensive parsing
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
- final exerciseList = decoded['exercises'] as List<dynamic>;
+ final exerciseList = decoded['exercises'];
+ if (exerciseList is! List) {
+ throw FormatException('Invalid response: missing exercises array');
+ }
final exercises = exerciseList
- .map((e) => Exercise.fromJson(e as Map<String, dynamic>))
+ .whereType<Map<String, dynamic>>()
+ .map((e) => Exercise.fromJson(e))
.toList();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final decoded = jsonDecode(response.body) as Map<String, dynamic>; | |
| final exerciseList = decoded['exercises'] as List<dynamic>; | |
| final exercises = exerciseList | |
| .map((e) => Exercise.fromJson(e as Map<String, dynamic>)) | |
| .toList(); | |
| final decoded = jsonDecode(response.body) as Map<String, dynamic>; | |
| final exerciseList = decoded['exercises']; | |
| if (exerciseList is! List) { | |
| throw FormatException('Invalid response: missing exercises array'); | |
| } | |
| final exercises = exerciseList | |
| .whereType<Map<String, dynamic>>() | |
| .map((e) => Exercise.fromJson(e)) | |
| .toList(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@workout-logger/lib/services/workout_provider.dart` around lines 299 - 303,
The parsing of the remote response assumes decoded['exercises'] exists and is a
List, causing a type cast exception on malformed JSON; update the code around
decoded, exerciseList and the mapping to first check that decoded is a
Map<String, dynamic>, that decoded.containsKey('exercises') and that
decoded['exercises'] is a List (not null), and if not either return an empty
exercises list or throw a clear, specific exception / log a descriptive error
before the generic catch; then safely cast each item and use Exercise.fromJson
only after verifying each item is a Map<String, dynamic> to avoid runtime cast
errors.
Summary
This PR adds the ability to fetch and cache a curated library of exercises from a remote GitHub JSON file, expanding the exercise database beyond built-in and custom exercises. Users can now download additional exercises with a single tap, and the app intelligently deduplicates exercises across all three sources (built-in, custom, remote).
Key Changes
fetchRemoteExercises()method toWorkoutProviderthat downloads exercises from a GitHub-hosted JSON file with timeout handling and error reportinggetAllExercises()andgetExercise()inStorageServiceto merge built-in, custom, and remote exercises with built-in/custom taking priority over remote when IDs collide_remoteExercisesBoxHive storage and corresponding interface methods (saveRemoteExercises,getRemoteExercises,clearRemoteExercises)exercises.jsonwith 10 curated exercises (Cable Crossover, Meadows Row, Z-Press, etc.) with proper muscle activation dataMockStorageServiceto support remote exercises with the same deduplication logicImplementation Details
remote_and displayed with secondary color theming_ExerciseCardand_ExerciseDetailsSheet) now handle three exercise types with appropriate visual differentiationhttps://claude.ai/code/session_01U3WJ7yBDYzNbYpo7LyDfzW
Summary by CodeRabbit
New Features
Tests