diff --git a/CHANGELOG.md b/CHANGELOG.md index f185363..eefebd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added button routing to HR Zones config on the home screen - Added info panel displaying the initial sync status - Enabled account deletion +- Added a toggle button to switch between time and percentage views for weekly summaries #### Changed - Changed time in zone calculation to be based on moving time instead of elapsed time diff --git a/backend/api/templates/api/changelog.html b/backend/api/templates/api/changelog.html index 9d1c64f..b5e4bec 100644 --- a/backend/api/templates/api/changelog.html +++ b/backend/api/templates/api/changelog.html @@ -76,6 +76,7 @@

Added

  • Button routing to HR Zones config on the home screen
  • Info panel displaying the initial sync status
  • Enabled account deletion
  • +
  • Added a toggle button to switch between time and percentage views for weekly summaries
  • Changed

    diff --git a/extension/content.js b/extension/content.js index 211972c..21eb407 100644 --- a/extension/content.js +++ b/extension/content.js @@ -1,6 +1,11 @@ const MONTHLY_SUMMARY_ID = 'hr-zones-monthly-summary-container'; const WEEKLY_SUMMARY_CLASS = 'injected-weekly-hr-summary'; +// --- State --- +let showWeeklyAsPercentage = true; +let currentMonthData = null; // To store data for re-renders + + const PRODUCTION_DOMAIN = 'https://strava-zones.com'; const DEVELOPMENT_DOMAIN = 'https://localhost:8000'; const IS_PRODUCTION_BUILD = false; // Set to true for production builds @@ -119,78 +124,124 @@ function renderMonthlySummary(monthKey, data) { } contentHtml += ''; summaryContainer.innerHTML = contentHtml; + + // Add toggle for weekly view + const toggleContainer = document.createElement('div'); + toggleContainer.className = 'strava-zones-toggle-container'; + toggleContainer.innerHTML = ` + + + `; + summaryContainer.appendChild(toggleContainer); + + const toggleInput = document.getElementById('weekly-view-toggle-input'); + const toggleLabel = document.getElementById('weekly-view-toggle-label'); + + function updateToggleLabel() { + if (toggleLabel) { + toggleLabel.textContent = showWeeklyAsPercentage ? 'Weekly: by %' : 'Weekly: by time'; + } + } + + if (toggleInput) { + updateToggleLabel(); // Set initial state + toggleInput.addEventListener('change', (event) => { + showWeeklyAsPercentage = event.target.checked; + updateToggleLabel(); + if (currentMonthData) { + renderWeeklySummaries(currentMonthData); + } + }); + } + console.log(`Monthly summary for ${monthKey} rendered.`); } function renderWeeklySummaries(data) { + // Clear previously injected weekly summaries to handle re-renders + document.querySelectorAll('.strava-zones-weekly-summary-cell').forEach(el => el.remove()); + const weekRowSelector = 'table.month-calendar.marginless tbody tr'; const weekRows = document.querySelectorAll(weekRowSelector); - if (!weekRows.length) { - console.warn(`No week rows found with selector: '${weekRowSelector}'. Weekly summaries not rendered.`); - return; - } - - // Dynamically generate orderedSimplifiedZoneKeys from data.zoneDefinitions - const simplifiedZoneKeys = Object.keys(data.zoneDefinitions || {}); - if (simplifiedZoneKeys.length === 0) { - console.warn("No zone definitions found in data.zoneDefinitions for weekly summaries. Cannot render."); + if (!weekRows.length || !data.weeklySummaries) { + console.warn('ZoneLens: Calendar week rows or weekly summaries not found for rendering.'); return; } - simplifiedZoneKeys.sort((a, b) => { + const orderedSimplifiedZoneKeys = Object.keys(data.zoneDefinitions || {}).sort((a, b) => { const numA = parseInt(a.replace('zone', ''), 10); const numB = parseInt(b.replace('zone', ''), 10); - return numA - numB; + return numB - numA; // descending }); - const orderedSimplifiedZoneKeys = simplifiedZoneKeys.reverse(); - weekRows.forEach((row, index) => { - if (data.weeklySummaries && data.weeklySummaries[index] && data.weeklySummaries[index].zone_times_seconds) { - const weeklyActivityZoneTimes = data.weeklySummaries[index].zone_times_seconds; + if (orderedSimplifiedZoneKeys.length === 0) { + console.warn("No zone definitions found for weekly summaries."); + return; + } - let totalWeekSeconds = 0; - for (const key of orderedSimplifiedZoneKeys) { - const actualName = data.zoneDefinitions[key]; - if (actualName && weeklyActivityZoneTimes[actualName]) { - totalWeekSeconds += weeklyActivityZoneTimes[actualName]; + let maxSingleZoneTimeInMonth = 0; + if (!showWeeklyAsPercentage) { + for (const week of data.weeklySummaries) { + if (week.zone_times_seconds) { + for (const time of Object.values(week.zone_times_seconds)) { + if (time > maxSingleZoneTimeInMonth) { + maxSingleZoneTimeInMonth = time; + } } } + } + } - if (totalWeekSeconds > 0) { - const panelContainer = document.createElement('div'); - panelContainer.className = WEEKLY_SUMMARY_CLASS; + weekRows.forEach((row, index) => { + if (data.weeklySummaries[index] && data.weeklySummaries[index].zone_times_seconds) { + const weeklyActivityZoneTimes = data.weeklySummaries[index].zone_times_seconds; + const totalWeekSeconds = Object.values(weeklyActivityZoneTimes).reduce((sum, time) => sum + time, 0); - let panelHtml = ''; + if (totalWeekSeconds > 0) { + let summaryHtml = ''; for (const simplifiedZoneKey of orderedSimplifiedZoneKeys) { const zoneNumberStr = simplifiedZoneKey.replace('zone', ''); + const zoneName = data.zoneDefinitions[simplifiedZoneKey] || `Zone ${zoneNumberStr}`; + const timeInZone = weeklyActivityZoneTimes[zoneName] || 0; - const actualUserDefinedName = data.zoneDefinitions[simplifiedZoneKey]; - const timeSeconds = actualUserDefinedName ? (weeklyActivityZoneTimes[actualUserDefinedName] || 0) : 0; + const percentage = totalWeekSeconds > 0 ? (timeInZone / totalWeekSeconds) * 100 : 0; + const timeFormatted = formatSecondsToHms(timeInZone); - const percentage = totalWeekSeconds > 0 ? (timeSeconds / totalWeekSeconds) * 100 : 0; - const timeFormatted = formatSecondsToHms(timeSeconds); + let barWidthPercentage; + if (showWeeklyAsPercentage) { + barWidthPercentage = percentage; + } else { + barWidthPercentage = maxSingleZoneTimeInMonth > 0 ? (timeInZone / maxSingleZoneTimeInMonth) * 100 : 0; + } - panelHtml += ` -
    - Z${zoneNumberStr} -
    -
    + summaryHtml += ` +
    +
    Z${zoneNumberStr}
    +
    +
    +
    +
    ${timeFormatted} (${percentage.toFixed(0)}%)
    - ${timeFormatted} (${percentage.toFixed(0)}%) -
    `; } - panelContainer.innerHTML = panelHtml; - // Create a new table cell (td) to hold our summary panel - const summaryCell = document.createElement('td'); - summaryCell.className = 'strava-zones-weekly-summary-cell'; - summaryCell.style.verticalAlign = 'top'; - summaryCell.style.padding = '2px'; + if (summaryHtml) { + const panelContainer = document.createElement('div'); + panelContainer.className = WEEKLY_SUMMARY_CLASS; + panelContainer.innerHTML = summaryHtml; - summaryCell.appendChild(panelContainer); - row.appendChild(summaryCell); // Add the new cell to the row (tr) + const summaryCell = document.createElement('td'); + summaryCell.className = 'strava-zones-weekly-summary-cell'; + summaryCell.style.verticalAlign = 'top'; + summaryCell.style.padding = '2px'; + + summaryCell.appendChild(panelContainer); + row.appendChild(summaryCell); + } } } }); @@ -309,6 +360,7 @@ async function initHrZoneDisplay() { zoneDefinitions: data.zone_definitions || {}, weeklySummaries: data.weekly_summaries || [] }; + currentMonthData = displayData; renderMonthlySummary(monthKey, displayData); renderWeeklySummaries(displayData); } else { diff --git a/extension/styles.css b/extension/styles.css index 4abfafe..3b88653 100644 --- a/extension/styles.css +++ b/extension/styles.css @@ -153,3 +153,81 @@ tr > td.strava-zones-weekly-summary-cell + td.strava-zones-weekly-summary-cell { display: none !important; } + +.strava-zones-toggle-container { + margin-top: 15px; + padding-top: 10px; + border-top: 1px solid #eee; + display: flex; + align-items: center; +} + +.strava-zones-toggle-container span { + margin-left: 10px; + font-size: 0.9em; + color: #555; + font-family: var(--sz-font-family); +} + +/* The switch - the box around the slider */ +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: var(--zone2-color); +} + +input:focus + .slider { + box-shadow: 0 0 1px var(--zone2-color); +} + +input:checked + .slider:before { + -webkit-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 24px; +} + +.slider.round:before { + border-radius: 50%; +}