Skip to content
Open
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
114 changes: 114 additions & 0 deletions exercises.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"version": 1,
"exercises": [
{
"id": "remote_cable_crossover",
"name": "Cable Crossover",
"category": "isolation",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "chest", "activationPercentage": 80 },
{ "muscleGroupId": "front_delts", "activationPercentage": 20 }
]
},
{
"id": "remote_meadows_row",
"name": "Meadows Row",
"category": "compound",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "back", "activationPercentage": 70 },
{ "muscleGroupId": "rear_delts", "activationPercentage": 25 },
{ "muscleGroupId": "biceps", "activationPercentage": 20 }
]
},
{
"id": "remote_z_press",
"name": "Z-Press",
"category": "compound",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "front_delts", "activationPercentage": 70 },
{ "muscleGroupId": "side_delts", "activationPercentage": 30 },
{ "muscleGroupId": "triceps", "activationPercentage": 20 },
{ "muscleGroupId": "core", "activationPercentage": 40 }
]
},
{
"id": "remote_larsen_press",
"name": "Larsen Press",
"category": "compound",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "chest", "activationPercentage": 65 },
{ "muscleGroupId": "triceps", "activationPercentage": 35 },
{ "muscleGroupId": "front_delts", "activationPercentage": 25 },
{ "muscleGroupId": "core", "activationPercentage": 30 }
]
},
{
"id": "remote_bayesian_curl",
"name": "Bayesian Cable Curl",
"category": "isolation",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "biceps", "activationPercentage": 90 },
{ "muscleGroupId": "forearms", "activationPercentage": 20 }
]
},
{
"id": "remote_nordic_curl",
"name": "Nordic Hamstring Curl",
"category": "isolation",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "hamstrings", "activationPercentage": 95 },
{ "muscleGroupId": "glutes", "activationPercentage": 20 }
]
},
{
"id": "remote_snatch_grip_rdl",
"name": "Snatch-Grip Romanian Deadlift",
"category": "compound",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "hamstrings", "activationPercentage": 75 },
{ "muscleGroupId": "glutes", "activationPercentage": 50 },
{ "muscleGroupId": "lower_back", "activationPercentage": 40 },
{ "muscleGroupId": "traps", "activationPercentage": 30 }
]
},
{
"id": "remote_copenhagen_plank",
"name": "Copenhagen Plank",
"category": "isolation",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "core", "activationPercentage": 60 },
{ "muscleGroupId": "hamstrings", "activationPercentage": 50 },
{ "muscleGroupId": "glutes", "activationPercentage": 40 }
]
},
{
"id": "remote_tib_raise",
"name": "Tibialis Raise",
"category": "isolation",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "calves", "activationPercentage": 90 }
]
},
{
"id": "remote_wide_grip_cable_row",
"name": "Wide-Grip Cable Row",
"category": "compound",
"isCustom": false,
"muscleActivations": [
{ "muscleGroupId": "back", "activationPercentage": 70 },
{ "muscleGroupId": "rear_delts", "activationPercentage": 35 },
{ "muscleGroupId": "biceps", "activationPercentage": 25 },
{ "muscleGroupId": "traps", "activationPercentage": 20 }
]
}
]
}
143 changes: 130 additions & 13 deletions workout-logger/lib/screens/exercise_library_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,29 @@ class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
String _searchQuery = '';
String? _selectedMuscleGroup;

Future<void> _fetchRemoteExercises(BuildContext context) async {
final provider = context.read<WorkoutProvider>();
await provider.fetchRemoteExercises();
if (!mounted) return;
final error = provider.lastFetchError;
if (error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to fetch exercises: $error')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Loaded ${provider.lastFetchCount} remote exercises'),
),
);
}
}

@override
Widget build(BuildContext context) {
// Use Provider's exercise list (includes custom exercises)
final allExercises = context.watch<WorkoutProvider>().allExercises;
final provider = context.watch<WorkoutProvider>();
// Use Provider's exercise list (includes custom + remote exercises)
final allExercises = provider.allExercises;

// Filter exercises
var filteredExercises = allExercises.where((e) {
Expand All @@ -38,13 +57,12 @@ class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
return matchesSearch && matchesMuscle;
}).toList();

// Sort: custom exercises first within each group for visibility
// Sort: custom first, remote second, then alphabetically
filteredExercises.sort((a, b) {
// First by custom status (custom first)
if (a.isCustom && !b.isCustom) return -1;
if (!a.isCustom && b.isCustom) return 1;
// Then alphabetically
return a.name.compareTo(b.name);
int rank(Exercise e) =>
e.isCustom ? 0 : e.id.startsWith('remote_') ? 1 : 2;
Comment on lines +62 to +63

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.

final cmp = rank(a).compareTo(rank(b));
return cmp != 0 ? cmp : a.name.compareTo(b.name);
});

// Group by primary muscle
Expand All @@ -61,6 +79,23 @@ class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
appBar: AppBar(
title: const Text('Exercise Library'),
actions: [
if (provider.isFetchingRemote)
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
else
IconButton(
icon: const Icon(Icons.cloud_download_outlined),
tooltip: 'Fetch exercises',
onPressed: () => _fetchRemoteExercises(context),
),
if (customCount > 0)
Padding(
padding: const EdgeInsets.only(right: AppSpacing.md),
Expand Down Expand Up @@ -260,7 +295,7 @@ class _ExerciseCard extends StatelessWidget {
padding: const EdgeInsets.all(AppSpacing.md),
child: Row(
children: [
// Icon with custom badge
// Icon with custom/remote badge
Stack(
children: [
Container(
Expand All @@ -269,7 +304,9 @@ class _ExerciseCard extends StatelessWidget {
decoration: BoxDecoration(
color: exercise.isCustom
? AppTheme.warning.withOpacity(0.2)
: AppTheme.primaryColor.withOpacity(0.2),
: exercise.id.startsWith('remote_')
? AppTheme.secondaryColor.withOpacity(0.2)
: AppTheme.primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Expand All @@ -278,7 +315,9 @@ class _ExerciseCard extends StatelessWidget {
: Icons.accessibility_new,
color: exercise.isCustom
? AppTheme.warning
: AppTheme.primaryColor,
: exercise.id.startsWith('remote_')
? AppTheme.secondaryColor
: AppTheme.primaryColor,
),
),
if (exercise.isCustom)
Expand All @@ -297,6 +336,23 @@ class _ExerciseCard extends StatelessWidget {
color: Colors.black,
),
),
)
else if (exercise.id.startsWith('remote_'))
Positioned(
right: -2,
top: -2,
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: AppTheme.secondaryColor,
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.cloud_done,
size: 10,
color: Colors.black,
),
),
),
],
),
Expand Down Expand Up @@ -363,6 +419,26 @@ class _ExerciseCard extends StatelessWidget {
),
),
),
] else if (exercise.id.startsWith('remote_')) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'REMOTE',
style: TextStyle(
color: AppTheme.secondaryColor,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
const SizedBox(width: 8),
Text(
Expand Down Expand Up @@ -437,7 +513,9 @@ class _ExerciseDetailsSheet extends StatelessWidget {
decoration: BoxDecoration(
color: exercise.isCustom
? AppTheme.warning.withOpacity(0.2)
: AppTheme.primaryColor.withOpacity(0.2),
: exercise.id.startsWith('remote_')
? AppTheme.secondaryColor.withOpacity(0.2)
: AppTheme.primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Expand All @@ -446,7 +524,9 @@ class _ExerciseDetailsSheet extends StatelessWidget {
: Icons.accessibility_new,
color: exercise.isCustom
? AppTheme.warning
: AppTheme.primaryColor,
: exercise.id.startsWith('remote_')
? AppTheme.secondaryColor
: AppTheme.primaryColor,
),
),
if (exercise.isCustom)
Expand All @@ -465,6 +545,23 @@ class _ExerciseDetailsSheet extends StatelessWidget {
color: Colors.black,
),
),
)
else if (exercise.id.startsWith('remote_'))
Positioned(
right: -2,
top: -2,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: AppTheme.secondaryColor,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.cloud_done,
size: 10,
color: Colors.black,
),
),
),
],
),
Expand Down Expand Up @@ -505,6 +602,26 @@ class _ExerciseDetailsSheet extends StatelessWidget {
),
),
),
] else if (exercise.id.startsWith('remote_')) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'REMOTE',
style: TextStyle(
color: AppTheme.secondaryColor,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ abstract class IStorageService {
Future<List<Exercise>> getAllExercises();
Future<Exercise?> getExercise(String id);

// ==================== REMOTE EXERCISES ====================

Future<void> saveRemoteExercises(List<Exercise> exercises);
Future<List<Exercise>> getRemoteExercises();
Future<void> clearRemoteExercises();

// ==================== SETTINGS ====================

Future<void> saveSetting(String key, String value);
Expand Down
Loading