Skip to content

Add remote exercise library fetching from GitHub - #34

Open
Devasy wants to merge 2 commits into
mainfrom
claude/dynamic-exercise-loader-jlaBv
Open

Add remote exercise library fetching from GitHub#34
Devasy wants to merge 2 commits into
mainfrom
claude/dynamic-exercise-loader-jlaBv

Conversation

@Devasy

@Devasy Devasy commented Apr 16, 2026

Copy link
Copy Markdown
Owner

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

  • Remote Exercise Fetching: Added fetchRemoteExercises() method to WorkoutProvider that downloads exercises from a GitHub-hosted JSON file with timeout handling and error reporting
  • Exercise Deduplication: Updated getAllExercises() and getExercise() in StorageService to merge built-in, custom, and remote exercises with built-in/custom taking priority over remote when IDs collide
  • UI Enhancements:
    • Added cloud download button in the Exercise Library AppBar that shows a loading spinner while fetching
    • Added visual badges and icons to distinguish remote exercises (cloud icon) from custom exercises (star icon)
    • Updated sorting to prioritize custom exercises first, then remote, then built-in, all alphabetically within each group
  • Storage Layer: Added new _remoteExercisesBox Hive storage and corresponding interface methods (saveRemoteExercises, getRemoteExercises, clearRemoteExercises)
  • Remote Exercise Data: Added exercises.json with 10 curated exercises (Cable Crossover, Meadows Row, Z-Press, etc.) with proper muscle activation data
  • Test Support: Updated MockStorageService to support remote exercises with the same deduplication logic

Implementation Details

  • Remote exercises are identified by IDs prefixed with remote_ and displayed with secondary color theming
  • Fetch operation includes 15-second timeout and displays user-friendly error messages via SnackBar
  • The fetch count is tracked and displayed to users on successful completion
  • Exercise Library UI components (_ExerciseCard and _ExerciseDetailsSheet) now handle three exercise types with appropriate visual differentiation

https://claude.ai/code/session_01U3WJ7yBDYzNbYpo7LyDfzW

Summary by CodeRabbit

  • New Features

    • Added a predefined exercise library with 10+ exercises including Cable Crossover, Meadows Row, Z-Press, and more.
    • Introduced cloud-based exercise download functionality; remote exercises are visually distinguished in the exercise library.
  • Tests

    • Updated test infrastructure to support remote exercise testing.

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
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Introduces 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

Cohort / File(s) Summary
Remote Exercises Data
exercises.json
New configuration file defining 10 predefined exercises with muscle activation details, serving as the remote data source.
Storage Infrastructure
workout-logger/lib/services/interfaces/storage_service_interface.dart, workout-logger/lib/services/storage_service.dart
Added Hive-based storage for remote exercises; new methods saveRemoteExercises(), getRemoteExercises(), and clearRemoteExercises() introduced; updated getAllExercises() and getExercise() to include and deduplicate remote exercises.
Remote Fetching & State Management
workout-logger/lib/services/workout_provider.dart
Implemented fetchRemoteExercises() method performing HTTP GET from GitHub with 15s timeout; added state tracking via isFetchingRemote, lastFetchError, and lastFetchCount getters.
Exercise Library UI
workout-logger/lib/screens/exercise_library_screen.dart
Added fetch remote exercises action in AppBar; updated filtering/sorting to prioritize custom exercises, then remote exercises (identified by id prefix), then others; added distinct visual styling and "REMOTE" labels for remote exercises.
Test Infrastructure
workout-logger/test/test_utils/mock_storage_service.dart
Extended MockStorageService with remote exercise storage support; implemented interface methods and added addMockRemoteExercise() test helper; updated getAllExercises() to include remote exercises with ID-based deduplication.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately describes the main feature: adding functionality to fetch remote exercises from GitHub. It is specific, concise, and directly reflects the primary objective and scope of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔵 Trivial

Note: 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

📥 Commits

Reviewing files that changed from the base of the PR and between f7c5b8c and 7940b22.

📒 Files selected for processing (6)
  • exercises.json
  • workout-logger/lib/screens/exercise_library_screen.dart
  • workout-logger/lib/services/interfaces/storage_service_interface.dart
  • workout-logger/lib/services/storage_service.dart
  • workout-logger/lib/services/workout_provider.dart
  • workout-logger/test/test_utils/mock_storage_service.dart

Comment on lines +62 to +63
int rank(Exercise e) =>
e.isCustom ? 0 : e.id.startsWith('remote_') ? 1 : 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +311 to +319
Future<void> saveRemoteExercises(List<Exercise> exercises) async {
await _remoteExercisesBoxInstance.clear();
for (final exercise in exercises) {
await _remoteExercisesBoxInstance.put(
exercise.id,
jsonEncode(exercise.toJson()),
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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).

Comment on lines +299 to +303
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants