Skip to content

Instantly share code, notes, and snippets.

@cobryan05
Created January 23, 2024 22:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cobryan05/382ead6c53702136e2d1f63de4d214b3 to your computer and use it in GitHub Desktop.
Save cobryan05/382ead6c53702136e2d1f63de4d214b3 to your computer and use it in GitHub Desktop.
TamperMonkey script to filter GarminConnect workout list by muscle
// ==UserScript==
// @name Garmin Connect Workout - Filter By Target Muscle
// @namespace https://github.com/cobryan05
// @version 2024-01-23
// @description Filter workouts by target area
// @author cobryan05
// @match https://connect.garmin.com/modern/workout/edit/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=garmin.com
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
// Variable to store all workouts
let all_workouts = null;
// Variable to store all unique muscles
let all_muscles = null;
function normalizeString(input) {
// Replace spaces with underscores
let result = input.replace(/[\s-]/g, '_');
// Convert to uppercase
result = result.toUpperCase();
// Prepend an _ if it starts with a digit
result = result.replace(/^\d/, match => `_${match}`);
return result;
}
function haveCommonItems(arr1, arr2) {
// Check if any element in arr1 is included in arr2
return arr1.some(item => arr2.includes(item));
}
// Function to filter exercises by muscle (primary or secondary)
function filterExercisesByMuscle(muscle, muscleType) {
const filteredExercises = [];
// Iterate through categories and exercises
for (const categoryKey in all_workouts.categories) {
const category = all_workouts.categories[categoryKey];
for (const exerciseKey in category.exercises) {
const exercise = category.exercises[exerciseKey];
// Check if the muscle is present in primary or secondary muscles
if (exercise[muscleType].includes(muscle)) {
filteredExercises.push(exerciseKey);
}
}
}
return filteredExercises;
}
function findMuscleGroupsByWorkoutName(workoutName) {
// Iterate through categories and exercises
for (const categoryKey in all_workouts.categories) {
const category = all_workouts.categories[categoryKey];
for (const exerciseKey in category.exercises) {
const exercise = category.exercises[exerciseKey];
// Check if the current exercise matches the given workout name
var plusCat = categoryKey.replace("_EXERCISES", "") + "_" + exerciseKey;
if (exerciseKey === workoutName || plusCat === workoutName) {
return {
primaryMuscles: exercise.primaryMuscles,
secondaryMuscles: exercise.secondaryMuscles
};
}
}
}
// Return null if the workout name is not found
return null;
}
// Function to be executed when a new dropdown is added
function handleDropdownChange(mutation) {
// Access the all_workouts and all_muscles variables here
if (all_workouts && all_muscles) {
var target = findAncestorWithClassName(mutation[0].target, "workout-step-in-edit-view");
var filter_list = target.getElementsByClassName("muscle-filter-list")[0];
var selected_boxes = filter_list.querySelectorAll('input[type="checkbox"]:checked');
var selected_ids = []
selected_boxes.forEach(function(item) { selected_ids.push(item.id); } );
var dropdown_list = mutation[0].addedNodes;
var use_colors = document.getElementById("color_filter_list").checked;
dropdown_list.forEach(function(item) {
if( item.classList.contains("active-result" ) ) {
var normalized = normalizeString(item.innerHTML);
var muscle_groups = findMuscleGroupsByWorkoutName(normalized);
if( muscle_groups != null ) {
var isPrimary = haveCommonItems( muscle_groups.primaryMuscles, selected_ids );
var isSecondary = haveCommonItems( muscle_groups.secondaryMuscles, selected_ids );
if( isPrimary ) {
if( use_colors ) {
item.style.backgroundColor = "green";
}
} else if( isSecondary ) {
if( use_colors ) {
item.style.backgroundColor = "grey";
}
} else {
item.style.display = 'none';
}
} else {
if( !selected_ids.includes("") ) {
item.style.display = 'none';
}
}
}
} );
}
}
// Function to add a multi-select dropdown list with checkboxes inside a nice box
function addMultiSelectDropdown(targetDiv) {
// Remove existing elements with the class "muscle-filter-list"
const existingDropdowns = document.querySelectorAll('.muscle-filter-list');
existingDropdowns.forEach(existingDropdown => existingDropdown.remove());
// Create a new div for the multi-select dropdown
const multiSelectDiv = document.createElement('div');
multiSelectDiv.classList.add('multi-select-dropdown');
multiSelectDiv.classList.add('muscle-filter-list');
// Create a table for the checkboxes
const table = document.createElement('table');
table.style.borderCollapse = 'collapse';
// Create a table body
const tbody = document.createElement('tbody');
let row; // Initialize row here
// Create checkboxes for each option and add them to the table
var idx = 0;
all_muscles.forEach(function (muscle) {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = muscle;
checkbox.id = muscle;
checkbox.checked = true;
// Create a label for the checkbox
const label = document.createElement('label');
label.textContent = muscle;
// Create a table cell for the checkbox and label
const cell = document.createElement('td');
cell.style.border = '1px solid black';
cell.style.padding = '5px';
// Append the checkbox and label to the cell
cell.appendChild(checkbox);
cell.appendChild(label);
// Create a new row for every 4 checkboxes
if (idx % 4 === 0) {
row = document.createElement('tr'); // Create a new row
row.appendChild(cell);
tbody.appendChild(row);
} else {
// Append the cell to the current row
row.appendChild(cell);
}
idx++;
});
// Append the last row if the number of checkboxes is not a multiple of 4
if (idx % 4 !== 0) {
tbody.appendChild(row);
}
// Append the table body to the table
table.appendChild(tbody);
// Append the table to the container div
multiSelectDiv.appendChild(table);
// Create "Select All" button
const selectAllButton = document.createElement('button');
selectAllButton.textContent = 'Select All';
selectAllButton.addEventListener('click', function () {
all_muscles.forEach(function (muscle) {
const checkbox = document.getElementById(muscle);
checkbox.checked = true;
});
});
// Create "Select None" button
const selectNoneButton = document.createElement('button');
selectNoneButton.textContent = 'Select None';
selectNoneButton.addEventListener('click', function () {
all_muscles.forEach(function (muscle) {
const checkbox = document.getElementById(muscle);
checkbox.checked = false;
});
});
// Append buttons to the multi-select dropdown div
multiSelectDiv.appendChild(selectAllButton);
multiSelectDiv.appendChild( document.createElement("br") );
multiSelectDiv.appendChild(selectNoneButton);
const colorLabel = document.createElement('label');
colorLabel.textContent = "Colorize Primary/Secondary";
const colorCheckbox = document.createElement('input');
colorCheckbox.type = 'checkbox';
colorCheckbox.value = "Colorize";
colorCheckbox.id = "color_filter_list";
colorCheckbox.checked = true;
colorCheckbox.label = "Test";
multiSelectDiv.appendChild( document.createElement("br") );
multiSelectDiv.appendChild(colorCheckbox);
multiSelectDiv.appendChild(colorLabel);
// Append the multi-select dropdown div to the target div
targetDiv.appendChild(multiSelectDiv);
}
// Function to download JSON data and store it in all_workouts variable
function downloadWorkoutsData() {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://connect.garmin.com/web-data/exercises/Exercises.json',
onload: function(response) {
if (response.status === 200) {
try {
// Parse JSON and store it in the variable
all_workouts = JSON.parse(response.responseText);
// Extract all unique muscles from the data
extractAllMuscles();
// Now that the data and muscles are available, start observing for changes
startObserving();
} catch (error) {
console.error('Error parsing JSON:', error);
}
} else {
console.error('Error fetching JSON. Status:', response.status);
}
},
onerror: function(error) {
console.error('GM_xmlhttpRequest error:', error);
}
});
}
// Function to start observing for changes in the DOM
function startObserving() {
// MutationObserver to watch for changes in the DOM
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// Check if a node has been added
if( mutation.type === 'childList' ) {
if (mutation.target.classList.contains('chosen-results')) {
// Call the function when a new dropdown is added
handleDropdownChange(mutations);
} else if (mutation.target.classList.contains('workout-step-in-edit-view')) {
addMultiSelectDropdown(mutation.addedNodes[0]);
}
}
});
});
// Options for the MutationObserver (configuring it to watch for childList changes)
const observerConfig = {
childList: true,
subtree: true
};
// Start observing the body for changes
observer.observe(document.body, observerConfig);
}
// Download the workouts data when the script is executed
downloadWorkoutsData();
// Function to find the first ancestor with a specific class name
function findAncestorWithClassName(node, className) {
let current = node.parentNode;
while (current) {
if (current.classList && current.classList.contains(className)) {
return current;
}
current = current.parentNode;
}
return null; // If no ancestor with the specified class name is found
}
// Function to extract all unique muscles from the data
function extractAllMuscles() {
all_muscles = new Set();
// Iterate through categories and exercises
for (const categoryKey in all_workouts.categories) {
const category = all_workouts.categories[categoryKey];
for (const exerciseKey in category.exercises) {
const exercise = category.exercises[exerciseKey];
// Add primary and secondary muscles to the set
all_muscles = new Set([...all_muscles, ...exercise.primaryMuscles, ...exercise.secondaryMuscles]);
}
}
// Convert the set to an array for easier access
all_muscles = Array.from(all_muscles);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment