Created
January 23, 2024 22:11
-
-
Save cobryan05/382ead6c53702136e2d1f63de4d214b3 to your computer and use it in GitHub Desktop.
TamperMonkey script to filter GarminConnect workout list by muscle
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
// ==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