Skip to content

Instantly share code, notes, and snippets.

@1337-server
Created November 2, 2025 04:08
Show Gist options
  • Select an option

  • Save 1337-server/60b272ded062d1db76d9c1e0a2d61cbf to your computer and use it in GitHub Desktop.

Select an option

Save 1337-server/60b272ded062d1db76d9c1e0a2d61cbf to your computer and use it in GitHub Desktop.
MyFitnessPal Diary Smart Widget (v3.0.0) - Fancy, compact diary with animated macro bars per meal, anomaly highlighting, badges, and dark mode. Injected after the dashboard. Uses __NEXT_DATA__ diary array + exercise energy value.
// ==UserScript==
// @name MyFitnessPal Diary Smart Widget (v3.0.0)
// @namespace mfp-dashboard
// @version 3.0.0
// @description Fancy, compact diary with animated macro bars per meal, anomaly highlighting, badges, and dark mode. Injected after the dashboard. Uses __NEXT_DATA__ diary array + exercise energy value.
// @author 1337-server
// @match https://www.myfitnesspal.com/*
// @run-at document-end
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ---------- Utils ----------
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
const safeNum = (x) => (typeof x === 'number' && isFinite(x) ? x : 0);
async function waitForNextData(retries = 24) {
return new Promise((resolve) => {
const attempt = () => {
const el = document.querySelector('#__NEXT_DATA__');
if (el) {
try {
const json = JSON.parse(el.textContent);
const queries = json?.props?.pageProps?.dehydratedState?.queries;
if (Array.isArray(queries)) return resolve(queries);
} catch (_) {}
}
if (--retries > 0) setTimeout(attempt, 500);
else resolve(null);
};
attempt();
});
}
function findDiary(queries) {
const today = new Date().toISOString().slice(0, 10);
return (
queries.find(
(q) =>
Array.isArray(q.queryKey) &&
q.queryKey[0] === 'diary' &&
String(q.queryKey[1]).startsWith(today)
) || null
);
}
function groupByMeal(entries) {
const meals = {};
let steps = 0;
let exerciseCalories = 0;
for (const entry of entries) {
if (entry.type === 'food_entry') {
const meal = entry.meal_name || 'Other';
if (!meals[meal]) meals[meal] = [];
meals[meal].push(entry);
} else if (entry.type === 'steps_aggregate') {
steps += safeNum(entry.steps);
} else if (entry.type === 'exercise_entry') {
// use actual recorded energy burned
exerciseCalories += safeNum(entry.energy?.value);
}
}
return { meals, steps, exerciseCalories };
}
function kcalFromMacros(c, p, f) {
return safeNum(c) * 4 + safeNum(p) * 4 + safeNum(f) * 9;
}
function computeMealTotals(items) {
let c = 0, p = 0, f = 0, kc = 0;
for (const it of items) {
const n = it.nutritional_contents || {};
c += safeNum(n.carbohydrates);
p += safeNum(n.protein);
f += safeNum(n.fat);
kc += safeNum(n.energy?.value);
}
const macroKcal = kcalFromMacros(c, p, f);
// prefer macro-derived calories if present; fall back to sum of energy
const mealKcal = macroKcal > 0 ? macroKcal : kc;
const pct = mealKcal > 0
? {
carbs: clamp((c * 4 * 100) / mealKcal, 0, 100),
protein: clamp((p * 4 * 100) / mealKcal, 0, 100),
fat: clamp((f * 9 * 100) / mealKcal, 0, 100),
}
: { carbs: 0, protein: 0, fat: 0 };
return {
carbs_g: c, protein_g: p, fat_g: f,
kcal: Math.round(mealKcal),
pct,
};
}
function classifyBadge(pct) {
const { carbs, protein, fat } = pct;
const max = Math.max(carbs, protein, fat);
if (max < 45 && max > 35) return { emoji: '⚡', label: 'Balanced' };
if (protein >= 35) return { emoji: '🥩', label: 'High Protein' };
if (fat >= 45) return { emoji: '🧈', label: 'High Fat' };
if (carbs >= 55) return { emoji: '🍞', label: 'High Carb' };
// fallback to dominant
if (max === protein) return { emoji: '🥩', label: 'High Protein' };
if (max === fat) return { emoji: '🧈', label: 'High Fat' };
return { emoji: '🍞', label: 'High Carb' };
}
function anomalyClass(food) {
const n = food.nutritional_contents || {};
const sugar = safeNum(n.sugar);
const fat = safeNum(n.fat);
const sodium = safeNum(n.sodium);
const kcal = safeNum(n.energy?.value);
// red: strong anomaly
if (sugar > 15 || fat > 15 || sodium > 600) return { cls: 'mfp-anom-red', why: `High ${sugar>15?'sugar':fat>15?'fat':'sodium'}` };
// amber: low protein density (kcal > 200 but protein < 10g)
if (kcal > 200 && safeNum(n.protein) < 10) return { cls: 'mfp-anom-amber', why: 'Low protein density' };
return null;
}
function cssOnce() {
if (document.getElementById('mfp-diary-css')) return;
const style = document.createElement('style');
style.id = 'mfp-diary-css';
style.textContent = `
:root {
--mfp-bg: #ffffff;
--mfp-text: #111111;
--mfp-sub: #555555;
--mfp-border: #e5e9f0;
--mfp-accent: #007aff;
--mfp-red: #ffd9de;
--mfp-amber: #fff2cc;
--mfp-red-border: #ff5a72;
--mfp-amber-border: #ffcc66;
--mfp-bar-bg: #eef3fb;
--mfp-carb: #5ba6ff;
--mfp-prot: #6bd48d;
--mfp-fat: #f7b267;
}
@media (prefers-color-scheme: dark) {
:root {
--mfp-bg: #1c1c1e;
--mfp-text: #f5f5f7;
--mfp-sub: #bdbdc2;
--mfp-border: #2c2c2e;
--mfp-accent: #4da3ff;
--mfp-red: #3a1f27;
--mfp-amber: #3a2f1f;
--mfp-red-border: #ff6f91;
--mfp-amber-border: #ffc166;
--mfp-bar-bg: #2a2a2c;
--mfp-carb: #5ba6ff;
--mfp-prot: #4ccc85;
--mfp-fat: #f0a85d;
}
}
.mfp-diary {
font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 760px;
margin: 24px auto;
padding: 18px 20px;
background: var(--mfp-bg);
color: var(--mfp-text);
border-radius: 16px;
border: 1px solid var(--mfp-border);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
animation: mfpFadeIn 0.4s ease-out;
}
@keyframes mfpFadeIn { from {opacity: 0; transform: translateY(4px);} to {opacity: 1; transform: translateY(0);} }
.mfp-diary h3 {
display: flex; align-items: center; gap: 8px;
font-weight: 700; font-size: 18px; color: var(--mfp-accent);
margin: 0 0 12px 0;
}
.mfp-meal { border-top: 1px solid var(--mfp-border); padding: 10px 0; }
.mfp-meal:first-child { border-top: none; padding-top: 0; }
.mfp-meal-header {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
margin-bottom: 6px;
}
.mfp-meal-title {
font-weight: 700; font-size: 15px; color: var(--mfp-text); display: inline-flex; align-items: center; gap: 6px;
}
.mfp-badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; color: var(--mfp-accent);
background: rgba(0,120,255,0.12); border: 1px solid rgba(0,120,255,0.25);
text-transform: none;
}
.mfp-meal-item {
display: flex; justify-content: space-between; gap: 12px;
font-size: 13px; color: var(--mfp-sub); padding: 2px 0;
}
.mfp-meal-item.mfp-anom-red {
background: var(--mfp-red); border-left: 3px solid var(--mfp-red-border); color: #7a0014;
border-radius: 6px; padding: 4px 6px; margin: 2px 0;
}
.mfp-meal-item.mfp-anom-amber {
background: var(--mfp-amber); border-left: 3px solid var(--mfp-amber-border); color: #8a5a00;
border-radius: 6px; padding: 4px 6px; margin: 2px 0;
}
/* Bars */
.mfp-bars { display: grid; grid-template-columns: 1fr; gap: 6px; margin-top: 8px; }
.mfp-bar {
width: 100%; height: 10px; background: var(--mfp-bar-bg); border-radius: 999px; overflow: hidden; position: relative;
}
.mfp-bar > span {
display: block; height: 100%; width: 0%;
border-radius: 999px; transform-origin: left center;
animation: mfpFill 900ms ease forwards;
}
@keyframes mfpFill { from { width: 0%; } to { width: var(--to, 0%); } }
.mfp-carb { background: var(--mfp-carb); }
.mfp-prot { background: var(--mfp-prot); }
.mfp-fat { background: var(--mfp-fat); }
.mfp-bar-labels {
display: flex; justify-content: space-between; font-size: 11px; color: var(--mfp-sub); margin-top: 2px;
}
.mfp-totals {
border-top: 2px solid var(--mfp-accent);
margin-top: 12px; padding-top: 8px; font-weight: 700;
display: flex; justify-content: space-between; color: var(--mfp-accent);
}
.mfp-extra {
margin-top: 6px; font-size: 13px; color: var(--mfp-sub);
display: flex; justify-content: space-between;
}
`;
document.head.appendChild(style);
}
function renderMeal(mealName, items) {
const totals = computeMealTotals(items);
const badge = classifyBadge(totals.pct);
const wrap = document.createElement('div');
wrap.className = 'mfp-meal';
const header = document.createElement('div');
header.className = 'mfp-meal-header';
header.innerHTML = `
<div class="mfp-meal-title">${mealName}
<span class="mfp-badge">${badge.emoji} ${badge.label}</span>
</div>
<div class="mfp-meal-kcal">${totals.kcal} kcal</div>
`;
wrap.appendChild(header);
// Items list with anomaly highlighting
for (const f of items) {
const brand = f.food?.brand_name ? `${f.food.brand_name} - ` : '';
const desc = f.food?.description || 'Food Item';
const kcal = safeNum(f.nutritional_contents?.energy?.value);
const anomaly = anomalyClass(f);
const row = document.createElement('div');
row.className = 'mfp-meal-item' + (anomaly ? ` ${anomaly.cls}` : '');
if (anomaly) row.title = anomaly.why;
row.innerHTML = `
<span>${brand}${desc}</span>
<span>${Math.round(kcal)} kcal</span>
`;
wrap.appendChild(row);
}
// Animated nutrient bars (percent of this meal’s calories)
const bars = document.createElement('div');
bars.className = 'mfp-bars';
bars.innerHTML = `
<div class="mfp-bar" aria-label="Carbs">
<span class="mfp-carb" style="--to:${totals.pct.carbs.toFixed(1)}%"></span>
<div class="mfp-bar-labels">
<span>Carbs ${totals.carbs_g.toFixed(1)}g</span>
<span>${totals.pct.carbs.toFixed(0)}%</span>
</div>
</div>
<div class="mfp-bar" aria-label="Protein">
<span class="mfp-prot" style="--to:${totals.pct.protein.toFixed(1)}%"></span>
<div class="mfp-bar-labels">
<span>Protein ${totals.protein_g.toFixed(1)}g</span>
<span>${totals.pct.protein.toFixed(0)}%</span>
</div>
</div>
<div class="mfp-bar" aria-label="Fat">
<span class="mfp-fat" style="--to:${totals.pct.fat.toFixed(1)}%"></span>
<div class="mfp-bar-labels">
<span>Fat ${totals.fat_g.toFixed(1)}g</span>
<span>${totals.pct.fat.toFixed(0)}%</span>
</div>
</div>
`;
wrap.appendChild(bars);
return wrap;
}
function renderDiaryWidget(entries) {
const { meals, steps, exerciseCalories } = groupByMeal(entries);
const card = document.createElement('div');
card.className = 'mfp-diary';
card.innerHTML = `<h3>🍽️ Today’s Food Diary</h3>`;
// Per-meal sections
for (const [mealName, items] of Object.entries(meals)) {
card.appendChild(renderMeal(mealName, items));
}
// Totals across all meals (simple, keeps original footer)
let tCal = 0, tC = 0, tP = 0, tF = 0;
for (const items of Object.values(meals)) {
for (const it of items) {
const n = it.nutritional_contents || {};
tCal += safeNum(n.energy?.value);
tC += safeNum(n.carbohydrates);
tP += safeNum(n.protein);
tF += safeNum(n.fat);
}
}
const totals = document.createElement('div');
totals.className = 'mfp-totals';
totals.innerHTML = `
<span>Total</span>
<span>${Math.round(tCal)} kcal • C:${Math.round(tC)}g P:${Math.round(tP)}g F:${Math.round(tF)}g</span>
`;
card.appendChild(totals);
const extra = document.createElement('div');
extra.className = 'mfp-extra';
extra.innerHTML = `
<span>🚶 Steps: ${steps.toLocaleString()}</span>
<span>🔥 Exercise: ${Math.round(exerciseCalories)} kcal</span>
`;
card.appendChild(extra);
return card;
}
async function inject() {
cssOnce();
const queries = await waitForNextData();
if (!queries) return console.warn('❌ __NEXT_DATA__ not found');
const diaryQ = findDiary(queries);
const diaryData = diaryQ?.state?.data;
if (!Array.isArray(diaryData)) return console.warn('⚠️ Diary data not in expected array form');
const card = renderDiaryWidget(diaryData);
if (!card) return console.warn('⚠️ Failed to render diary card');
// inject AFTER the last dashboard section to avoid nesting into grid
const sections = document.querySelectorAll('[data-testid^="qa-regression-"]');
const last = sections[sections.length - 1];
if (last) {
last.insertAdjacentElement('afterend', card);
console.log('✅ Injected diary after last dashboard section');
} else {
document.body.appendChild(card);
console.log('✅ Injected diary at body (fallback)');
}
}
window.addEventListener('load', () => setTimeout(inject, 1800));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment