// ==UserScript==
// @name Strava Feed Filter
// @name:en Strava Feed Filter
// @description Advanced filtering for your Strava activity feed: keywords, activity types, distance, duration, elevation, pace, map presence; draggable UI; real-time updates.
// @description:en Advanced filtering for your Strava activity feed: keywords, activity types, distance, duration, elevation, pace, map presence; draggable UI; real-time updates.
// @namespace https://github.com/Inc21/Tempermonkey-Strava-Feed-Filter
// @version 0.2.3
// @license MIT
// @author Inc21
// @match https://www.strava.com/*
// @grant GM_addStyle
// @run-at document-end
// @homepageURL https://github.com/Inc21/Tempermonkey-Strava-Feed-Filter
// @supportURL https://github.com/Inc21/Tempermonkey-Strava-Feed-Filter/issues
// @downloadURL https://raw.githubusercontent.com/Inc21/Tempermonkey-Strava-Feed-Filter/main/userscript/strava-feed-filter-clean.js
// @updateURL https://raw.githubusercontent.com/Inc21/Tempermonkey-Strava-Feed-Filter/main/userscript/strava-feed-filter-clean.js
// ==/UserScript==
(function() {
'use strict';
/*
* Copyright (c) 2025 Inc21
* Licensed under the MIT License. See LICENSE file in the project root for full license text.
*/
console.log('🚀 Clean Filter: Script starting...');
const STORAGE_KEY = "stravaFeedFilter";
const POS_KEY = "stravaFeedFilterPos";
const DEFAULTS = {
keywords: [],
allowedAthletes: [],
types: {},
hideNoMap: false,
hideGiveGift: false,
hideClubPosts: false,
hideChallenges: false,
hideJoinedChallenges: false,
hideSuggestedFriends: false,
hideYourClubs: false,
hideMyWindsock: false,
hideSummitbag: false,
hideRunHealth: false,
hideFooter: false,
showKudosButton: false,
minKm: 0,
maxKm: 0,
minMins: 0,
maxMins: 0,
minElevM: 0,
maxElevM: 0,
minPace: 0,
maxPace: 0,
unitSystem: 'metric', // 'metric' or 'imperial'
enabled: true
};
const TYPES = [
{ key: "Ride", label: "Ride" },
{ key: "Walk", label: "Walk" },
{ key: "VirtualRide", label: "Virtual Ride" },
{ key: "Run", label: "Run" },
{ key: "Swim", label: "Swim" },
{ key: "Hike", label: "Hike" },
{ key: "TrailRun", label: "Trail Run" },
{ key: "MountainBikeRide", label: "Mountain Bike Ride" },
{ key: "GravelRide", label: "Gravel Ride" },
{ key: "EBikeRide", label: "E-Bike Ride" },
{ key: "EMountainBikeRide", label: "E-Mountain Bike Ride" },
{ key: "AlpineSki", label: "Alpine Ski" },
{ key: "Badminton", label: "Badminton" },
{ key: "BackcountrySki", label: "Backcountry Ski" },
{ key: "Canoeing", label: "Canoe" },
{ key: "Crossfit", label: "Crossfit" },
{ key: "Elliptical", label: "Elliptical" },
{ key: "Golf", label: "Golf" },
{ key: "IceSkate", label: "Ice Skate" },
{ key: "InlineSkate", label: "Inline Skate" },
{ key: "Handcycle", label: "Handcycle" },
{ key: "HighIntensityIntervalTraining", label: "HIIT" },
{ key: "Kayaking", label: "Kayaking" },
{ key: "Kitesurf", label: "Kitesurf" },
{ key: "NordicSki", label: "Nordic Ski" },
{ key: "Pickleball", label: "Pickleball" },
{ key: "Pilates", label: "Pilates" },
{ key: "Racquetball", label: "Racquetball" },
{ key: "RockClimbing", label: "Rock Climb" },
{ key: "RollerSki", label: "Roller Ski" },
{ key: "Rowing", label: "Rowing" },
{ key: "Sail", label: "Sail" },
{ key: "Skateboard", label: "Skateboard" },
{ key: "Snowboard", label: "Snowboard" },
{ key: "Snowshoe", label: "Snowshoe" },
{ key: "Soccer", label: "Football (Soccer)" },
{ key: "Squash", label: "Squash" },
{ key: "StandUpPaddling", label: "Stand Up Paddling" },
{ key: "StairStepper", label: "Stair-Stepper" },
{ key: "Surfing", label: "Surfing" },
{ key: "TableTennis", label: "Table Tennis" },
{ key: "Tennis", label: "Tennis" },
{ key: "Velomobile", label: "Velomobile" },
{ key: "VirtualRun", label: "Virtual Run" },
{ key: "VirtualRow", label: "Virtual Rowing" },
{ key: "WeightTraining", label: "Weight Training" },
{ key: "Windsurf", label: "Windsurf" },
{ key: "Wheelchair", label: "Wheelchair" },
{ key: "Workout", label: "Workout" },
{ key: "Yoga", label: "Yoga" }
];
// CSS Module - Step 1 of modular refactoring
function injectStyles() {
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
.sff-clean-btn {
position: fixed !important;
top: 10px !important;
right: 10px !important;
z-index: 2147483647 !important;
padding: 5px 12px !important;
background: #fc5200 !important;
color: white !important;
border: 1px solid transparent !important;
cursor: pointer !important;
font-weight: 700 !important;
border-radius: 4px !important;
font-family: 'Roboto', sans-serif !important;
text-align: center !important;
transition: background-color 0.15s ease !important;
display: inline-flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 1px !important;
line-height: 1.2 !important;
}
.sff-clean-btn:hover {
background: #e04a00 !important;
}
.sff-btn-title {
font-size: 14px !important;
line-height: 1.1 !important;
text-transform: uppercase !important;
padding: 3px 6px !important;
margin-left: 6px !important;
}
.sff-clean-btn .sff-btn-sub {
font-weight: 500 !important;
text-transform: uppercase !important;
color: white !important;
opacity: 1 !important;
line-height: 1 !important;
}
/* Drop the button earlier to avoid covering header buttons */
@media (max-width: 1460px) {
.sff-clean-btn {
top: 56px !important; /* drop below header a bit */
}
}
/* Switch button to left and up; move panel to left as well */
@media (max-width: 985px) {
.sff-clean-btn {
top: 10px !important;
right: auto !important;
left: 280px !important; /* shift by roughly button width to clear logo */
}
.sff-clean-panel {
right: auto !important;
left: 10px !important;
}
}
/* Even smaller screens: keep at top, but push further right to avoid burger */
@media (max-width: 760px) {
.sff-clean-btn {
top: 10px !important; /* remain at top */
left: 340px !important; /* push further right to clear burger */
right: auto !important;
}
}
.sff-clean-panel {
position: fixed !important;
top: 60px !important;
right: 10px !important;
z-index: 2147483646 !important;
width: 360px !important;
min-height: 180px !important;
max-height: 70vh !important;
background: white !important;
border: 2px solid #fc5200 !important;
border-radius: 8px !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.2) !important;
font-family: Arial, sans-serif !important;
overflow: visible !important;
display: none !important;
visibility: visible !important;
opacity: 1 !important;
transition: none !important;
}
.sff-clean-panel.show {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.sff-panel-header {
background: #fc5200 !important;
padding: 12px 16px !important;
border-bottom: none !important;
cursor: move !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
border-radius: 6px 6px 0 0 !important;
}
.sff-panel-header h3 {
margin: 0 !important;
font-size: 14px !important;
user-select: none !important;
color: white !important;
font-family: 'Poppins', 'Montserrat', sans-serif !important;
font-weight: 800 !important;
text-transform: uppercase !important;
}
.sff-header-main {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
width: 100%;
margin-right: 16px !important;
}
.sff-toggle-switch {
position: relative !important;
display: inline-block !important;
width: 34px !important;
height: 20px !important;
}
.sff-toggle-switch input {
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
.sff-slider {
position: absolute !important;
cursor: pointer !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background-color: #ccc !important;
transition: .4s !important;
border-radius: 20px !important;
}
.sff-slider:before {
position: absolute !important;
content: "" !important;
height: 14px !important;
width: 14px !important;
left: 3px !important;
bottom: 3px !important;
background-color: white !important;
transition: .4s !important;
border-radius: 50% !important;
}
input:checked + .sff-slider {
background-color: #4CAF50 !important;
}
input:checked + .sff-slider:before {
transform: translateX(14px) !important;
}
.sff-clean-panel .sff-section h4 {
margin: 0 0 10px 0 !important;
font-size: 13px !important;
color: #333 !important;
font-weight: 600 !important;
}
.sff-panel-header .sff-close {
background: none !important;
border: 1px solid white !important;
font-size: 22px !important;
color: white !important;
cursor: pointer !important;
padding: 2px 6px !important;
border-radius: 4px !important;
line-height: 1 !important;
}
.sff-panel-header .sff-close:hover {
background: rgba(255,255,255,0.2) !important;
color: #fc5200 !important;
}
.sff-panel-content {
padding: 16px !important;
max-height: calc(70vh - 100px) !important;
overflow-y: scroll !important;
}
.sff-clean-panel .sff-row {
margin: 0 0 16px 0 !important;
display: block !important;
}
.sff-clean-panel .sff-label {
display: block !important;
font-size: 14px !important;
margin-bottom: 6px !important;
font-weight: 500 !important;
color: #333 !important;
}
.sff-clean-panel .sff-input {
width: 100% !important;
padding: 8px 12px !important;
border: 1px solid #ddd !important;
border-radius: 6px !important;
font-size: 14px !important;
box-sizing: border-box !important;
background: white !important;
color: #333 !important;
}
.sff-clean-panel .sff-input:focus {
outline: none !important;
border-color: #fc5200 !important;
box-shadow: 0 0 0 2px rgba(252, 82, 0, 0.1) !important;
}
.sff-input-group {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: 8px !important;
}
.sff-unit-toggle {
display: flex !important;
border: 1px solid #ddd !important;
border-radius: 6px !important;
overflow: hidden !important;
}
.sff-unit-btn {
flex: 1 !important;
padding: 8px !important;
border: none !important;
background: #f7f7f7 !important;
cursor: pointer !important;
font-size: 13px !important;
transition: background-color 0.2s ease !important;
}
.sff-unit-btn:not(.active) {
background: white !important;
color: #555 !important;
}
.sff-unit-btn.active {
background: #fc5200 !important;
color: white !important;
font-weight: 600 !important;
}
.sff-unit-btn.metric {
border-right: 1px solid #ddd !important;
}
.sff-dropdown-header {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
cursor: pointer !important;
padding: 8px 12px !important;
border: 1px solid #ddd !important;
border-radius: 6px !important;
background: #f7f7f7 !important;
transition: background-color 0.2s ease !important;
}
.sff-dropdown-header:hover {
background: #eee !important;
}
.sff-dropdown.open .sff-dropdown-header {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.sff-dropdown-header .sff-label {
margin-bottom: 0 !important;
}
.sff-activity-count {
font-size: 12px !important;
color: #666 !important;
}
.sff-dropdown-content {
display: none; /* Initially hidden */
padding: 12px !important;
border: 1px solid #ddd !important;
border-top: none !important;
border-radius: 0 0 6px 6px !important;
margin-top: -1px !important;
}
.sff-dropdown-right {
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
.sff-dropdown-indicator {
transition: transform 0.2s ease !important;
}
.sff-dropdown.open .sff-dropdown-indicator {
transform: rotate(180deg) !important;
}
.sff-clean-panel .sff-keywords {
min-height: 40px !important;
max-height: 120px !important;
resize: vertical !important;
line-height: 1.4 !important;
}
.sff-types {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)) !important; /* Increased min width */
gap: 4px 8px !important; /* Increased gap */
margin-top: 3px !important;
}
.sff-clean-panel .sff-chip {
font-family: 'Roboto', sans-serif !important;
font-weight: 400 !important;
font-size: 14px !important; /* Increased for readability */
display: flex !important;
align-items: center !important;
padding: 4px 0 !important;
border: none !important;
border-radius: 0 !important;
line-height: 1 !important;
background: transparent !important;
cursor: pointer !important;
transition: none !important;
user-select: none !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.sff-clean-panel .sff-chip:hover {
background: transparent !important;
}
.sff-clean-panel .sff-chip.checked {
background: transparent !important;
color: #333 !important;
font-weight: 400 !important; /* Ensure it's not bold when checked */
}
.sff-clean-panel .sff-chip input {
margin-right: 4px !important;
margin-left: 0 !important;
transform: scale(0.85) !important;
}
.sff-switch {
position: relative !important;
display: inline-block !important;
width: 40px !important;
height: 22px !important;
}
.sff-switch input {
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
}
.sff-slider {
position: absolute !important;
cursor: pointer !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background-color: #ccc !important;
transition: .4s !important;
border-radius: 22px !important;
}
.sff-slider:before {
position: absolute !important;
content: "" !important;
height: 16px !important;
width: 16px !important;
left: 3px !important;
bottom: 3px !important;
background-color: white !important;
transition: .4s !important;
border-radius: 50% !important;
}
input:checked + .sff-slider {
background-color: #fc5200 !important;
}
input:checked + .sff-slider:before {
transform: translateX(18px) !important;
}
.sff-toggle-section {
display: flex !important;
align-items: center !important;
gap: 10px !important;
margin-bottom: 16px !important;
}
.sff-clean-panel .sff-section {
margin-bottom: 16px !important;
}
.sff-clean-panel .sff-buttons {
display: flex !important;
gap: 8px !important;
justify-content: center !important;
margin-top: 16px !important;
padding-top: 12px !important;
border-top: 1px solid #eee !important;
}
.sff-header-kudos-btn {
padding: 6px 12px !important; /* Align with gift button */
background: #fc5200 !important;
color: white !important;
border: 1px solid transparent !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 14px !important;
font-weight: 700 !important;
text-decoration: none !important;
font-family: 'Roboto', sans-serif !important;
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
line-height: 1.2 !important;
transition: background-color 0.15s ease !important;
}
.sff-header-kudos-btn:hover {
background: #e04a00 !important;
}
.sff-desc {
font-size: 11px !important;
color: #666 !important;
margin: -2px 0 8px 22px !important;
}
.sff-clean-panel .sff-buttons button {
padding: 6px 12px !important;
border: 1px solid #ddd !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 12px !important;
font-weight: 500 !important;
}
.sff-clean-panel .sff-save {
background: #fc5200 !important;
color: white !important;
border-color: #fc5200 !important;
}
.sff-clean-panel .sff-save:hover {
background: #e04700 !important;
border-color: #e04700 !important;
}
.sff-clean-panel .sff-reset {
background: white !important;
color: #fc5200 !important;
border-color: #fc5200 !important;
}
.sff-clean-panel .sff-reset:hover {
background: rgba(252, 82, 0, 0.05) !important;
color: #e04700 !important;
border-color: #e04700 !important;
}
.sff-footer {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
margin-top: 12px !important;
padding: 8px 12px !important;
background: #f8f9fa !important;
border: 1px solid #e9ecef !important;
border-radius: 6px !important;
gap: 8px !important;
}
.sff-credits {
text-align: left !important;
margin: 0 !important;
padding: 0 !important;
background: none !important;
border: none !important;
border-radius: 0 !important;
font-size: 11px !important;
color: #666 !important;
flex: 1 !important;
}
.sff-credits p {
margin: 0 !important;
line-height: 1.3 !important;
}
.sff-credits p:first-child {
font-weight: 600 !important;
color: #333 !important;
}
.sff-credits a {
color: #fc5200 !important;
text-decoration: none !important;
font-weight: 700 !important;
transition: color 0.2s ease !important;
}
.sff-credits a:hover {
color: #e04a00 !important;
text-decoration: underline !important;
}
.sff-bmc {
text-align: right !important;
margin: 0 !important;
padding: 0 !important;
border-top: none !important;
flex-shrink: 0 !important;
}
.sff-bmc a {
display: inline-block !important;
padding: 8px 16px !important;
background: #FC5200 !important;
color: #fff !important;
text-decoration: none !important;
border-radius: 6px !important;
font-size: 12px !important;
font-weight: 500 !important;
transition: all 0.2s !important;
}
.sff-bmc a:hover {
background: #e04a00 !important;
}
.sff-copyright {
text-align: center !important;
margin-top: 6px !important;
padding-top: 6px !important;
border-top: 1px solid #eee !important;
font-size: 9px !important;
color: #aaa !important;
}
.sff-copyright p {
margin: 0 !important;
line-height: 1.2 !important;
}
/* Secondary navigation row for smaller screens */
.sff-secondary-nav {
position: fixed !important;
top: 55px !important;
left: 0 !important;
right: 0 !important;
z-index: 10 !important;
background: white !important;
border-bottom: 1px solid #e5e5e5 !important;
padding: 8px 16px !important;
display: none !important;
justify-content: flex-end !important;
align-items: center !important;
gap: 12px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important;
}
/* Show secondary nav on smaller screens ONLY on dashboard */
@media (max-width: 1479px) {
body[data-sff-dashboard="true"] .sff-secondary-nav {
display: flex !important;
}
/* Hide main filter button on smaller screens ONLY on dashboard */
body[data-sff-dashboard="true"] .sff-clean-btn {
display: none !important;
}
/* Hide main header kudos button on smaller screens ONLY on dashboard */
body[data-sff-dashboard="true"] #gj-kudos-li {
display: none !important;
}
/* Adjust page content to account for secondary nav ONLY on dashboard */
body[data-sff-dashboard="true"] {
padding-top: 60px !important;
}
/* Additional margin for main content area to ensure no overlap */
body[data-sff-dashboard="true"] main,
body[data-sff-dashboard="true"] .view {
margin-top: 8px !important;
}
}
/* Secondary nav filter button */
.sff-secondary-filter-btn {
padding: 6px 12px !important;
background: #fc5200 !important;
color: white !important;
border: 1px solid transparent !important;
cursor: pointer !important;
font-weight: 700 !important;
border-radius: 4px !important;
font-family: 'Roboto', sans-serif !important;
text-align: center !important;
transition: background-color 0.15s ease !important;
font-size: 14px !important;
line-height: 1.2 !important;
text-transform: uppercase !important;
position: relative !important;
z-index: 1000 !important;
}
.sff-secondary-filter-btn:hover {
background: #e04a00 !important;
}
/* Secondary nav kudos button */
.sff-secondary-kudos-btn {
padding: 6px 12px !important;
background: #fc5200 !important;
color: white !important;
border: 1px solid transparent !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 14px !important;
font-weight: 700 !important;
text-decoration: none !important;
font-family: 'Roboto', sans-serif !important;
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
line-height: 1.2 !important;
transition: background-color 0.15s ease !important;
position: relative !important;
z-index: 1000 !important;
}
.sff-secondary-kudos-btn:hover {
background: #e04a00 !important;
}
`);
}
// Initialize CSS Module
injectStyles();
// Utilities Module - Step 2 of modular refactoring
const UtilsModule = {
// Settings management
loadSettings() {
let s;
try {
s = JSON.parse(localStorage.getItem(STORAGE_KEY));
} catch(e) {}
return s ? {...DEFAULTS, ...s} : {...DEFAULTS};
},
saveSettings(s) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
} catch(e) {
console.error('Error saving settings:', e);
}
},
// Debounce helper
debounce(fn, wait) {
let t;
return function(...args) {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), wait);
};
},
// Page detection
isOnDashboard() {
return window.location.pathname === '/dashboard' || window.location.pathname === '/';
},
// Data parsing utilities
parseDurationSeconds(activityEl) {
const timeLi = [...activityEl.querySelectorAll('li')].find(li => {
const label = li.querySelector('span');
return label && label.textContent.trim().toLowerCase() === 'time';
});
if (!timeLi) return null;
const value = timeLi.querySelector('.vNsSU') || timeLi;
if (!value) return null;
let h = 0, m = 0, s = 0;
const abbrs = value.querySelectorAll('abbr.unit');
if (!abbrs.length) {
const t = (value.textContent || '').trim();
if (!t) return null;
if (t.includes(':')) {
const parts = t.split(':').map(x => parseInt(x.trim(), 10));
if (parts.every(n => Number.isFinite(n))) {
if (parts.length === 3) [h, m, s] = parts;
else if (parts.length === 2) [m, s] = parts;
else if (parts.length === 1) m = parts[0];
return h * 3600 + m * 60 + s;
}
}
const maybe = parseFloat(t);
return Number.isFinite(maybe) ? Math.round(maybe * 60) : null;
}
abbrs.forEach(abbr => {
const unit = (abbr.getAttribute('title') || '').toLowerCase();
const numText = (abbr.previousSibling && abbr.previousSibling.textContent) ? abbr.previousSibling.textContent.trim() : '';
const num = parseInt(numText, 10);
if (!Number.isFinite(num)) return;
if (unit.includes('hour')) h = num;
else if (unit.includes('minute')) m = num;
else if (unit.includes('second')) s = num;
});
return h * 3600 + m * 60 + s;
},
parseDistanceKm(activityEl) {
const distLi = [...activityEl.querySelectorAll('li')].find(li => {
const label = li.querySelector('span');
return label && label.textContent.trim().toLowerCase() === 'distance';
});
if (!distLi) return null;
const value = distLi.querySelector('.vNsSU') || distLi;
if (!value) return null;
const abbr = value.querySelector('abbr.unit');
const text = (value.textContent || '').trim();
let num = NaN;
if (abbr && abbr.previousSibling && abbr.previousSibling.textContent) {
num = parseFloat(abbr.previousSibling.textContent.trim());
}
if (!Number.isFinite(num)) {
const m = text.match(/([0-9]+(?:\.[0-9]+)?)/);
if (m) num = parseFloat(m[1]);
}
if (!Number.isFinite(num)) return null;
const unitTitle = (abbr && abbr.getAttribute('title')) ? abbr.getAttribute('title').toLowerCase() : '';
if (unitTitle.includes('kilometer')) return num;
if (unitTitle.includes('mile')) return num * 1.60934;
if (unitTitle.includes('meter')) return num / 1000;
if (unitTitle.includes('yard')) return num * 0.0009144;
if (unitTitle.includes('foot') || unitTitle.includes('feet')) return num * 0.0003048;
return num; // assume km if unknown
},
parseElevationM(activityEl) {
const elevLi = [...activityEl.querySelectorAll('li')].find(li => {
const label = li.querySelector('span');
return label && (label.textContent.trim().toLowerCase() === 'elev gain' || label.textContent.trim().toLowerCase() === 'elevation gain');
});
if (!elevLi) return null;
const value = elevLi.querySelector('.vNsSU') || elevLi;
if (!value) return null;
const text = (value.textContent || '').trim().replace(/,/g, ''); // remove commas from thousands
let num = parseFloat(text);
if (!Number.isFinite(num)) return null;
const abbr = value.querySelector('abbr.unit');
const unitTitle = (abbr && abbr.getAttribute('title')) ? abbr.getAttribute('title').toLowerCase() : '';
if (unitTitle.includes('foot') || unitTitle.includes('feet')) return num * 0.3048;
// assume meters if no unit or meters
return num;
}
};
// UI Module - Step 3 of modular refactoring
const UIModule = {
updateActivityCount(panel) {
const countEl = panel.querySelector('.sff-activity-count');
if (!countEl) return;
const total = TYPES.length;
const hidden = panel.querySelectorAll('.sff-types input[type="checkbox"]:checked').length;
countEl.textContent = `(${hidden} hidden / ${total} total)`;
},
updateFilterLabels(panel, unitSystem) {
const isMetric = unitSystem === 'metric';
panel.querySelector('[data-label-type="distance"]').textContent = `Distance (${isMetric ? 'km' : 'mi'}):`;
panel.querySelector('[data-label-type="elevation"]').textContent = `Elevation Gain (${isMetric ? 'm' : 'ft'}):`;
panel.querySelector('[data-label-type="pace"]').textContent = `Pace for Runs (${isMetric ? 'min/km' : 'min/mi'}):`;
},
applySettings(panel) {
settings.keywords = panel.querySelector('.sff-keywords').value
.split(',')
.map(x => x.trim())
.filter(Boolean);
settings.allowedAthletes = panel.querySelector('.sff-allowed-athletes').value
.split(',')
.map(x => x.trim())
.filter(Boolean);
settings.minKm = +panel.querySelector('.sff-minKm').value || 0;
settings.maxKm = +panel.querySelector('.sff-maxKm').value || 0;
settings.minMins = +panel.querySelector('.sff-minMins').value || 0;
settings.maxMins = +panel.querySelector('.sff-maxMins').value || 0;
settings.minElevM = +panel.querySelector('.sff-minElevM').value || 0;
settings.maxElevM = +panel.querySelector('.sff-maxElevM').value || 0;
settings.minPace = +panel.querySelector('.sff-minPace').value || 0;
settings.maxPace = +panel.querySelector('.sff-maxPace').value || 0;
settings.unitSystem = panel.querySelector('.sff-unit-btn.active').dataset.unit;
settings.hideNoMap = panel.querySelector('.sff-hideNoMap').checked;
settings.hideClubPosts = panel.querySelector('.sff-hideClubPosts').checked;
settings.hideChallenges = panel.querySelector('.sff-hideChallenges').checked;
settings.hideJoinedChallenges = panel.querySelector('.sff-hideJoinedChallenges') ? panel.querySelector('.sff-hideJoinedChallenges').checked : settings.hideJoinedChallenges;
settings.hideSuggestedFriends = panel.querySelector('.sff-hideSuggestedFriends').checked;
settings.hideYourClubs = panel.querySelector('.sff-hideYourClubs').checked;
settings.hideMyWindsock = panel.querySelector('.sff-hideMyWindsock').checked;
settings.hideSummitbag = panel.querySelector('.sff-hideSummitbag').checked;
settings.hideRunHealth = panel.querySelector('.sff-hideRunHealth').checked;
settings.hideFooter = panel.querySelector('.sff-hideFooter') ? panel.querySelector('.sff-hideFooter').checked : settings.hideFooter;
settings.showKudosButton = panel.querySelector('.sff-showKudosButton').checked;
LogicModule.manageHeaderKudosButton(); // Update button immediately on apply
UIModule.syncSecondaryKudosVisibility(); // Sync secondary button visibility
const giftChk = panel.querySelector('.sff-hideGift');
settings.hideGiveGift = giftChk ? giftChk.checked : settings.hideGiveGift;
settings.types = {};
panel.querySelectorAll('input[type=checkbox][data-typ]').forEach(input => {
settings.types[input.dataset.typ] = input.checked;
});
UtilsModule.saveSettings(settings);
console.log('💾 Settings saved:', settings);
},
createElements() {
console.log('🔧 Clean Filter: Creating elements...');
// Remove existing
document.querySelectorAll('.sff-clean-btn, .sff-clean-panel, .sff-secondary-nav').forEach(el => el.remove());
// Only create elements on dashboard
const isDashboardPage = UtilsModule.isOnDashboard();
// Set dashboard attribute for CSS targeting
if (isDashboardPage) {
document.body.setAttribute('data-sff-dashboard', 'true');
} else {
document.body.removeAttribute('data-sff-dashboard');
// On non-dashboard pages, only apply global settings like hiding gift button and challenges
LogicModule.updateGiftVisibility();
LogicModule.updateChallengesVisibility();
LogicModule.updateSuggestedFriendsVisibility();
LogicModule.updateYourClubsVisibility();
LogicModule.updateMyWindsockVisibility();
LogicModule.updateSummitbagVisibility();
LogicModule.updateRunHealthVisibility();
return; // Exit early, no UI elements needed on non-dashboard pages
}
// Create secondary navigation row
const secondaryNav = document.createElement('div');
secondaryNav.className = 'sff-secondary-nav';
// Create secondary filter button
const secondaryFilterElement = document.createElement('button');
secondaryFilterElement.className = 'sff-secondary-filter-btn';
secondaryFilterElement.innerHTML = 'Filter (0)';
// Create secondary kudos button (will be shown/hidden based on settings)
const secondaryKudosElement = document.createElement('a');
secondaryKudosElement.className = 'sff-secondary-kudos-btn';
secondaryKudosElement.href = 'javascript:void(0);';
secondaryKudosElement.textContent = 'Give 👍 to Everyone';
// Use setProperty with !important to override CSS rules
secondaryKudosElement.style.setProperty('display', settings.showKudosButton ? 'inline-flex' : 'none', 'important');
secondaryNav.appendChild(secondaryKudosElement);
secondaryNav.appendChild(secondaryFilterElement);
document.body.appendChild(secondaryNav);
// Ensure secondary kudos button visibility is properly synchronized
this.syncSecondaryKudosVisibility();
// Create button
const btn = document.createElement('button');
btn.className = 'sff-clean-btn';
btn.innerHTML = 'Filter (0)';
btn.style.position = 'fixed';
btn.style.top = '10px';
btn.style.right = '10px';
btn.style.zIndex = '2147483647';
// Create panel using helper method
const panel = this._createPanel();
document.body.appendChild(btn);
document.body.appendChild(panel);
console.log('✅ Clean Filter: Elements added');
// Get secondary elements (we're only on dashboard at this point)
const secondaryFilterBtn = document.querySelector('.sff-secondary-filter-btn');
const secondaryKudosBtn = document.querySelector('.sff-secondary-kudos-btn');
this.setupEvents(btn, panel, secondaryFilterBtn, secondaryKudosBtn);
return { btn, panel, secondaryFilterBtn, secondaryKudosBtn };
},
// Synchronize secondary kudos button visibility with settings
syncSecondaryKudosVisibility() {
const secondaryKudosBtn = document.querySelector('.sff-secondary-kudos-btn');
if (secondaryKudosBtn) {
const shouldShow = settings.enabled && settings.showKudosButton;
// Use setProperty with !important to override CSS rules
secondaryKudosBtn.style.setProperty('display', shouldShow ? 'inline-flex' : 'none', 'important');
console.log('🔄 Secondary kudos button visibility updated:', shouldShow ? 'visible' : 'hidden');
}
},
_createPanel() {
const panel = document.createElement('div');
panel.className = 'sff-clean-panel';
// Set initial styles - ensure it's hidden by default
panel.style.position = 'fixed';
panel.style.display = 'none';
panel.style.visibility = 'hidden';
panel.style.opacity = '0';
panel.style.zIndex = '2147483646';
panel.style.width = '320px';
panel.style.right = '10px';
panel.style.top = '60px';
panel.style.transition = 'opacity 0.2s ease, visibility 0.2s';
// Load position
const savedPos = JSON.parse(localStorage.getItem('sffPanelPos') || '{}');
if (savedPos.left || savedPos.top) {
panel.style.left = savedPos.left || '';
panel.style.right = savedPos.left ? 'auto' : '10px';
panel.style.top = savedPos.top || '60px';
}
// Build panel content sections
const header = this._createPanelHeader();
const content = this._createPanelContent();
panel.appendChild(header);
panel.appendChild(content);
return panel;
},
_createPanelHeader() {
const header = document.createElement('div');
header.className = 'sff-panel-header';
header.innerHTML = `
Strava Feed Filter
`;
return header;
},
_createPanelContent() {
const content = document.createElement('div');
content.className = 'sff-panel-content';
content.innerHTML = this._getPanelHTML();
return content;
},
_getPanelHTML() {
return `
FILTER ${settings.enabled ? 'ON' : 'OFF'}
Report a bug or dead filter: HERE
`;
},
setupEvents(btn, panel, secondaryFilterBtn, secondaryKudosBtn) {
console.log('🎯 Clean Filter: Setting up events...');
// Initialize draggable
const cleanupDraggable = this.makeDraggable(panel);
// Load saved position
const savedPos = JSON.parse(localStorage.getItem('sffPanelPos') || '{}');
if (savedPos.left || savedPos.top) {
panel.style.left = savedPos.left || '';
panel.style.top = savedPos.top || '';
panel.style.right = savedPos.left ? 'auto' : '10px';
}
// Ensure panel is in viewport on load
setTimeout(() => this.keepInViewport(panel), 100);
// Handle window resize
let resizeTimeout;
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const wasVisible = panel.style.display === 'block';
if (wasVisible) {
panel.style.display = 'none';
}
// Sync secondary kudos button visibility on resize
this.syncSecondaryKudosVisibility();
// Force reflow to ensure proper measurements
void panel.offsetHeight;
// Update position to stay in viewport
this.keepInViewport(panel);
if (wasVisible) {
panel.style.display = 'block';
}
// Save new position
localStorage.setItem('sffPanelPos', JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
}, 100);
};
window.addEventListener('resize', handleResize);
// Handle click outside (define first)
const handleClickOutside = (e) => {
const clickedSecondaryBtn = secondaryFilterBtn && secondaryFilterBtn.contains(e.target);
if (!panel.contains(e.target) && !btn.contains(e.target) && !clickedSecondaryBtn) {
const isVisible = panel.style.display === 'block' && panel.style.visibility !== 'hidden';
if (isVisible) {
togglePanel();
}
}
};
// Toggle panel function
const togglePanel = () => {
const isVisible = panel.style.display === 'block' && panel.style.visibility !== 'hidden';
console.log('🔄 Toggle panel called. Currently visible:', isVisible);
if (!isVisible) {
console.log('📁 Showing panel...');
// Close all dropdowns before showing the panel
panel.querySelectorAll('.sff-dropdown.open').forEach(dropdown => {
dropdown.classList.remove('open');
const content = dropdown.querySelector('.sff-dropdown-content');
if (content) content.style.display = 'none';
});
// Position panel directly under the active button
const activeBtn = (window.innerWidth <= 1479 && secondaryFilterBtn) ? secondaryFilterBtn : btn;
const btnRect = activeBtn.getBoundingClientRect();
const gap = 5; // Small gap between button and panel
panel.style.left = btnRect.left + 'px';
panel.style.top = (btnRect.bottom + gap) + 'px';
panel.style.right = 'auto';
// Show panel
panel.style.display = 'block';
panel.style.visibility = 'visible';
panel.style.opacity = '1';
panel.classList.add('show');
// Ensure panel stays within viewport after positioning
this.keepInViewport(panel);
console.log('✅ Panel should now be visible');
// Add click outside handler
setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 0);
} else {
console.log('😫 Hiding panel...');
// Hide panel
panel.classList.remove('show');
panel.style.opacity = '0';
panel.style.visibility = 'hidden';
document.removeEventListener('click', handleClickOutside);
// After transition completes, update display
setTimeout(() => {
if (panel.style.visibility === 'hidden') {
panel.style.display = 'none';
}
}, 200);
}
};
// Toggle panel on button click
btn.addEventListener('click', (e) => {
console.log('🔥 Filter button clicked!');
e.stopPropagation();
togglePanel();
});
// Setup secondary filter button event (only if exists)
if (secondaryFilterBtn) {
secondaryFilterBtn.addEventListener('click', (e) => {
console.log('🔥 Secondary filter button clicked!');
e.stopPropagation();
togglePanel();
});
}
// Setup secondary kudos button event (only if exists)
if (secondaryKudosBtn) {
secondaryKudosBtn.addEventListener('click', () => {
let kudosGiven = 0;
const kudosButtons = document.querySelectorAll("button[data-testid='kudos_button']");
kudosButtons.forEach(button => {
const feedEntry = button.closest('.activity, .feed-entry, [data-testid="web-feed-entry"]');
if (feedEntry && feedEntry.style.display !== 'none' && button.title !== 'View all kudos') {
button.click();
kudosGiven++;
}
});
const originalText = secondaryKudosBtn.textContent;
secondaryKudosBtn.textContent = `Gave ${kudosGiven} 👍`;
secondaryKudosBtn.style.pointerEvents = 'none';
setTimeout(() => {
secondaryKudosBtn.textContent = originalText;
secondaryKudosBtn.style.pointerEvents = 'auto';
}, 3000);
});
}
// Close button
panel.querySelector('.sff-close').addEventListener('click', (e) => {
e.stopPropagation();
togglePanel();
});
// Main toggle switch
panel.querySelector('.sff-enabled-toggle').addEventListener('change', (e) => {
settings.enabled = e.target.checked;
UtilsModule.saveSettings(settings);
filterActivities();
});
// Toggle all dropdowns
panel.querySelectorAll('.sff-dropdown-header').forEach(header => {
header.addEventListener('click', (e) => {
const dropdown = e.currentTarget.closest('.sff-dropdown');
if (!dropdown) return;
const content = dropdown.querySelector('.sff-dropdown-content');
const isVisible = content.style.display === 'block';
content.style.display = isVisible ? 'none' : 'block';
dropdown.classList.toggle('open', !isVisible);
});
});
// Checkbox styling and real-time updates
panel.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
const chip = e.target.closest('.sff-chip');
if (chip) chip.classList.toggle('checked', e.target.checked);
// Master enable toggle
if (e.target.classList.contains('sff-enabled-toggle')) {
settings.enabled = e.target.checked;
UtilsModule.saveSettings(settings);
const toggleText = document.querySelector('.sff-toggle-text');
if (toggleText) toggleText.textContent = `FILTER ${settings.enabled ? 'ON' : 'OFF'}`;
LogicModule.applyAllFilters();
return; // Other toggles not relevant when flipping master
}
// Header kudos toggle
if (e.target.classList.contains('sff-showKudosButton')) {
settings.showKudosButton = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.manageHeaderKudosButton();
UIModule.syncSecondaryKudosVisibility();
}
// Gift button
if (e.target.classList.contains('sff-hideGift')) {
settings.hideGiveGift = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateGiftVisibility();
}
// Your challenges section
if (e.target.classList.contains('sff-hideChallenges')) {
settings.hideChallenges = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateChallengesVisibility();
LogicModule.filterActivities();
}
// Joined challenge cards
if (e.target.classList.contains('sff-hideJoinedChallenges')) {
settings.hideJoinedChallenges = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateJoinedChallengesVisibility();
LogicModule.filterActivities();
}
// Suggested Friends
if (e.target.classList.contains('sff-hideSuggestedFriends')) {
settings.hideSuggestedFriends = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateSuggestedFriendsVisibility();
LogicModule.filterActivities();
}
// Your Clubs
if (e.target.classList.contains('sff-hideYourClubs')) {
settings.hideYourClubs = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateYourClubsVisibility();
LogicModule.filterActivities();
}
// External embeds
if (e.target.classList.contains('sff-hideMyWindsock')) {
settings.hideMyWindsock = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateMyWindsockVisibility();
}
if (e.target.classList.contains('sff-hideSummitbag')) {
settings.hideSummitbag = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateSummitbagVisibility();
}
if (e.target.classList.contains('sff-hideRunHealth')) {
settings.hideRunHealth = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateRunHealthVisibility();
}
// Footer
if (e.target.classList.contains('sff-hideFooter')) {
settings.hideFooter = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.updateFooterVisibility();
}
// Activity visibility rules
if (e.target.classList.contains('sff-hideNoMap')) {
settings.hideNoMap = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.filterActivities();
}
if (e.target.classList.contains('sff-hideClubPosts')) {
settings.hideClubPosts = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.filterActivities();
}
// Activity type chips
if (e.target.hasAttribute('data-typ')) {
const typ = e.target.getAttribute('data-typ');
settings.types[typ] = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.filterActivities();
}
// Update count display
UIModule.updateActivityCount(panel);
}
});
// Apply button
panel.querySelector('.sff-save').addEventListener('click', () => {
console.log('💾 Applying and refreshing...');
this.applySettings(panel);
location.reload();
});
// Reset button
panel.querySelector('.sff-reset').addEventListener('click', () => {
if (confirm('Are you sure you want to reset all filters to their default values?')) {
settings = {...DEFAULTS};
UtilsModule.saveSettings(settings);
location.reload();
}
});
// Unit system toggle
panel.querySelector('.sff-unit-toggle').addEventListener('click', (e) => {
if (e.target.matches('.sff-unit-btn')) {
const newUnit = e.target.dataset.unit;
if (newUnit !== settings.unitSystem) {
settings.unitSystem = newUnit;
panel.querySelectorAll('.sff-unit-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.updateFilterLabels(panel, newUnit);
}
}
});
// Setup responsive behavior
this.setupWindowResize(panel);
this.setupButtonResponsive(btn);
this.updateActivityCount(panel);
this.updateFilterLabels(panel, settings.unitSystem);
console.log('✅ Events attached');
// Return cleanup function for when the script is unloaded
return () => {
window.removeEventListener('resize', handleResize);
cleanupDraggable && cleanupDraggable();
document.removeEventListener('click', handleClickOutside);
};
},
makeDraggable(panel) {
const header = panel.querySelector('.sff-panel-header');
if (!header) return () => {}; // Return empty cleanup if no header
let isDragging = false;
let startX, startY, startLeft, startTop;
const onMouseDown = (e) => {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return;
if (panel.style.visibility !== 'visible') return; // Only drag when visible
isDragging = true;
startX = e.clientX;
startY = e.clientY;
// Get current position without forcing reflow
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
header.style.cursor = 'grabbing';
document.body.style.userSelect = 'none'; // Prevent text selection
e.preventDefault();
e.stopPropagation();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// Calculate new position - allow free movement in all directions
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// Get viewport and panel dimensions
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const panelRect = panel.getBoundingClientRect();
// Keep panel within viewport bounds with margins from memory: 6px left, 20px right, 6px top/bottom
const leftMargin = 6;
const rightMargin = 20;
const topBottomMargin = 6;
newLeft = Math.max(leftMargin, Math.min(newLeft, viewportWidth - panelRect.width - rightMargin));
newTop = Math.max(topBottomMargin, Math.min(newTop, viewportHeight - panelRect.height - topBottomMargin));
// Apply new position with !important to override CSS (from memory: avoid style conflicts)
panel.style.setProperty('left', newLeft + 'px', 'important');
panel.style.setProperty('top', newTop + 'px', 'important');
panel.style.setProperty('right', 'auto', 'important');
panel.style.setProperty('bottom', 'auto', 'important');
// Save position
localStorage.setItem('sffPanelPos', JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
};
const onMouseUp = (e) => {
if (isDragging) {
isDragging = false;
header.style.cursor = '';
document.body.style.userSelect = ''; // Restore text selection
}
};
header.addEventListener('mousedown', onMouseDown, { passive: false });
document.addEventListener('mousemove', onMouseMove, { passive: false });
document.addEventListener('mouseup', onMouseUp, { passive: false });
// Cleanup function
return () => {
header.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Save final position
localStorage.setItem('sffPanelPos', JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
};
},
keepInViewport(panel) {
const rect = panel.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const panelWidth = rect.width;
const panelHeight = rect.height;
let left = parseInt(panel.style.left) || 0;
let top = parseInt(panel.style.top) || 0;
// Adjust if panel is outside viewport with margins from memory: 6px left, 20px right, 6px top/bottom
const leftMargin = 6;
const rightMargin = 20;
const topBottomMargin = 6;
if (left + panelWidth > viewportWidth) {
left = viewportWidth - panelWidth - rightMargin;
}
if (top + panelHeight > viewportHeight) {
top = viewportHeight - panelHeight - topBottomMargin;
}
if (left < 0) {
left = leftMargin;
}
if (top < 0) {
top = topBottomMargin;
}
// Apply new position
panel.style.left = left + 'px';
panel.style.top = top + 'px';
panel.style.right = 'auto';
// Save adjusted position
localStorage.setItem('sffPanelPos', JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
},
setupWindowResize(panel) {
window.addEventListener('resize', () => {
const rect = panel.getBoundingClientRect();
if (rect.right > window.innerWidth) {
panel.style.right = '10px';
}
if (rect.bottom > window.innerHeight) {
panel.style.top = Math.max(20, window.innerHeight - panel.offsetHeight - 20) + 'px';
}
if (rect.left < 0) {
panel.style.right = Math.max(0, window.innerWidth - panel.offsetWidth - 10) + 'px';
}
if (rect.top < 0) {
panel.style.top = '20px';
}
localStorage.setItem('sffPanelPos', JSON.stringify({
top: panel.style.top,
right: panel.style.right
}));
});
},
setupButtonResponsive(btn) {
const adjust = () => {
const w = window.innerWidth;
// When width <= 985px, button is on the left; adjust its left offset smoothly
if (w <= 985) {
const minW = 760; // at and below this, we want the MIN left offset (closest to logo)
const maxW = 985; // at this width, we want the MAX left offset
const minLeft = 180; // px (closest to logo on very small widths)
const maxLeft = 265; // px (more to the right when there's space)
let leftPx;
if (w <= minW) {
leftPx = minLeft; // smallest left when width is smallest
} else {
const t = (maxW - w) / (maxW - minW); // 0..1 as width shrinks
// As width shrinks, move left toward logo (decrease left value)
leftPx = Math.round(maxLeft - t * (maxLeft - minLeft));
}
btn.style.setProperty('left', leftPx + 'px', 'important');
btn.style.setProperty('right', 'auto', 'important');
btn.style.setProperty('top', '10px', 'important');
} else {
// Clear inline overrides so CSS media queries control right-mode positioning
btn.style.removeProperty('left');
btn.style.removeProperty('right');
btn.style.removeProperty('top');
}
};
// Run once now and on resize
adjust();
window.addEventListener('resize', adjust);
// Also run after a short delay to allow page header to stabilize
setTimeout(adjust, 250);
}
};
// Logic Module - Step 6 of modular refactoring
const LogicModule = {
// Determine if a feed node is a challenge card ("joined a challenge")
isChallengeEntry(node) {
if (!node) return false;
const el = (node.matches?.('[data-testid="web-feed-entry"]') ? node : node.closest?.('[data-testid="web-feed-entry"]')) || node;
// Common markers for challenges
if (el.matches?.('[data-testid="challenge-card"], .challenge-card')) return true;
// Join Challenge button present
const hasJoinBtn = el.querySelector?.('button, a') && Array.from(el.querySelectorAll('button, a')).some(b => (b.textContent || '').trim().toLowerCase().includes('join challenge'));
if (hasJoinBtn) return true;
// Links to /challenges/... without an owners-name (no athlete owner)
const hasChallengeLink = !!el.querySelector?.('a[href^="/challenges/"]');
const hasOwnerName = !!el.querySelector?.('[data-testid="owners-name"], .entry-athlete');
if (hasChallengeLink && !hasOwnerName) return true;
return false;
},
// Determine if a feed node is a real activity entry (not a challenge or club post)
isActivityEntry(node) {
if (!node) return false;
const el = (node.matches?.('[data-testid="web-feed-entry"]') ? node : node.closest?.('[data-testid="web-feed-entry"]')) || node;
// Must have activity icon or activity container
const hasIcon = !!el.querySelector?.('svg[data-testid="activity-icon"]');
const hasActivityContainer = !!el.querySelector?.('[data-testid="activity_entry_container"], .activity-name, .entry-title');
// Exclude challenge entries explicitly
return (hasIcon || hasActivityContainer) && !this.isChallengeEntry(el);
},
updateJoinedChallengesVisibility() {
try {
const entries = document.querySelectorAll('[data-testid="web-feed-entry"], .feed-entry, .activity');
entries.forEach(entry => {
if (this.isChallengeEntry(entry)) {
if (settings.enabled && settings.hideJoinedChallenges) {
if (entry.dataset.sffHiddenChallenge !== 'sff') {
entry.dataset.sffHiddenChallenge = 'sff';
entry.style.display = 'none';
}
} else if (entry.dataset.sffHiddenChallenge === 'sff') {
entry.style.display = '';
delete entry.dataset.sffHiddenChallenge;
}
}
});
} catch (e) {
console.warn('updateJoinedChallengesVisibility error:', e);
}
},
updateGiftVisibility() {
try {
const links = document.querySelectorAll('a[href*="/gift"][href*="origin=global_nav"]');
links.forEach(a => {
if (settings.enabled && settings.hideGiveGift) {
if (a.dataset.sffHiddenBy !== 'sff') {
a.dataset.sffHiddenBy = 'sff';
a.style.display = 'none';
}
} else if (a.dataset.sffHiddenBy === 'sff') {
a.style.display = '';
delete a.dataset.sffHiddenBy;
}
});
} catch (e) {
console.warn('updateGiftVisibility error:', e);
}
},
updateChallengesVisibility() {
try {
const challengesSection = document.querySelector('#your-challenges');
if (challengesSection) {
if (settings.enabled && settings.hideChallenges) {
if (challengesSection.dataset.sffHiddenBy !== 'sff') {
challengesSection.dataset.sffHiddenBy = 'sff';
challengesSection.style.display = 'none';
}
} else if (challengesSection.dataset.sffHiddenBy === 'sff') {
challengesSection.style.display = '';
delete challengesSection.dataset.sffHiddenBy;
}
}
} catch (e) {
console.warn('updateChallengesVisibility error:', e);
}
},
updateSuggestedFriendsVisibility() {
try {
const suggestedFriendsSection = document.querySelector('#suggested-follows');
if (suggestedFriendsSection) {
if (settings.enabled && settings.hideSuggestedFriends) {
if (suggestedFriendsSection.dataset.sffHiddenBy !== 'sff') {
suggestedFriendsSection.dataset.sffHiddenBy = 'sff';
suggestedFriendsSection.style.display = 'none';
}
} else if (suggestedFriendsSection.dataset.sffHiddenBy === 'sff') {
suggestedFriendsSection.style.display = '';
delete suggestedFriendsSection.dataset.sffHiddenBy;
}
}
} catch (e) {
console.warn('updateSuggestedFriendsVisibility error:', e);
}
},
updateYourClubsVisibility() {
try {
const yourClubsSection = document.querySelector('#your-clubs');
if (yourClubsSection) {
if (settings.enabled && settings.hideYourClubs) {
if (yourClubsSection.dataset.sffHiddenBy !== 'sff') {
yourClubsSection.dataset.sffHiddenBy = 'sff';
yourClubsSection.style.display = 'none';
}
} else if (yourClubsSection.dataset.sffHiddenBy === 'sff') {
yourClubsSection.style.display = '';
delete yourClubsSection.dataset.sffHiddenBy;
}
}
} catch (e) {
console.warn('updateYourClubsVisibility error:', e);
}
},
updateFooterVisibility() {
try {
// Find ONLY the footer section that includes footer-specific markers
const markerSelector = 'a[href*="/legal/terms"], a[href*="/legal/privacy"], a[href*="/legal/cookie_policy"], #language-picker, #cpra-compliance-cta';
let footerSection = Array.from(document.querySelectorAll('div.FvXwlgEO > section._01jT9FUf, section._01jT9FUf'))
.find(sec => sec.querySelector(markerSelector) || /©\s*\d{4}\s*Strava/i.test(sec.textContent || '')) || null;
if (!footerSection) {
// Fallback to canonical footer elements
footerSection = document.querySelector('footer, [data-testid="footer"], .global-footer, .site-footer');
}
if (!footerSection) return;
if (settings.enabled && settings.hideFooter) {
if (footerSection.dataset.sffHiddenBy !== 'sff') {
footerSection.dataset.sffHiddenBy = 'sff';
footerSection.style.setProperty('display', 'none', 'important');
footerSection.style.setProperty('margin', '0', 'important');
footerSection.style.setProperty('padding', '0', 'important');
footerSection.style.setProperty('height', '0', 'important');
}
} else if (footerSection.dataset.sffHiddenBy === 'sff') {
footerSection.style.removeProperty('display');
footerSection.style.removeProperty('margin');
footerSection.style.removeProperty('padding');
footerSection.style.removeProperty('height');
delete footerSection.dataset.sffHiddenBy;
}
} catch (e) {
console.warn('updateFooterVisibility error:', e);
}
},
updateMyWindsockVisibility() {
try {
const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]');
console.log(`🔍 Checking ${activities.length} activities for myWindsock content`);
activities.forEach(activity => {
// Find only text-containing elements (paragraphs and spans) that specifically contain myWindsock content
const textElements = activity.querySelectorAll('p, span, .text-content, .description-text, .activity-text');
textElements.forEach(element => {
const text = element.textContent?.trim() || '';
// Only hide if this element specifically contains the myWindsock report and not other content
if (text.includes('-- myWindsock Report --') && text.length < 500) { // Limit to avoid hiding large containers
console.log('🔮 Found myWindsock content in text element:', element);
if (settings.enabled && settings.hideMyWindsock) {
if (element.dataset.sffHiddenBy !== 'sff') {
element.dataset.sffHiddenBy = 'sff';
element.style.display = 'none';
console.log('🔮 myWindsock text content hidden:', element);
}
} else if (element.dataset.sffHiddenBy === 'sff') {
element.style.display = '';
delete element.dataset.sffHiddenBy;
}
}
});
});
} catch (e) {
console.warn('updateMyWindsockVisibility error:', e);
}
},
updateSummitbagVisibility() {
try {
const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]');
console.log(`🔍 Checking ${activities.length} activities for summitbag content`);
activities.forEach(activity => {
// Find only text-containing elements (paragraphs and spans) that specifically contain summitbag content
const textElements = activity.querySelectorAll('p, span, .text-content, .description-text, .activity-text');
textElements.forEach(element => {
const text = element.textContent?.trim() || '';
// Only hide if this element specifically contains summitbag and not other content
if (text.includes('summitbag.com') && text.length < 500) { // Limit to avoid hiding large containers
console.log('🏔️ Found summitbag content in text element:', element);
if (settings.enabled && settings.hideSummitbag) {
if (element.dataset.sffHiddenBy !== 'sff') {
element.dataset.sffHiddenBy = 'sff';
element.style.display = 'none';
console.log('🏔️ summitbag text content hidden:', element);
}
} else if (element.dataset.sffHiddenBy === 'sff') {
element.style.display = '';
delete element.dataset.sffHiddenBy;
}
}
});
});
} catch (e) {
console.warn('updateSummitbagVisibility error:', e);
}
},
updateRunHealthVisibility() {
try {
const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]');
console.log(`🔍 Checking ${activities.length} activities for Run Health content`);
activities.forEach(activity => {
// Find only text-containing elements (paragraphs and spans) that specifically contain Run Health content
const textElements = activity.querySelectorAll('p, span, .text-content, .description-text, .activity-text');
textElements.forEach(element => {
const text = element.textContent?.trim() || '';
// Only hide if this element specifically contains Run Health and not other content
if (text.includes('www.myTF.run') && text.length < 500) { // Limit to avoid hiding large containers
console.log('🏃 Found Run Health content in text element:', element);
if (settings.enabled && settings.hideRunHealth) {
if (element.dataset.sffHiddenBy !== 'sff') {
element.dataset.sffHiddenBy = 'sff';
element.style.display = 'none';
console.log('🏃 Run Health text content hidden:', element);
}
} else if (element.dataset.sffHiddenBy === 'sff') {
element.style.display = '';
delete element.dataset.sffHiddenBy;
}
}
});
});
} catch (e) {
console.warn('updateRunHealthVisibility error:', e);
}
},
// Count hidden sections for display in filter button
countHiddenSections() {
let hiddenSectionsCount = 0;
// Count hidden sections
const sectionsToCheck = [
{ selector: '#your-challenges', setting: 'hideChallenges' },
{ selector: '#suggested-follows', setting: 'hideSuggestedFriends' },
{ selector: '#your-clubs', setting: 'hideYourClubs' },
{ selector: 'div.FvXwlgEO', setting: 'hideFooter' }
];
sectionsToCheck.forEach(({ selector, setting }) => {
const section = document.querySelector(selector);
if (section && settings[setting] && section.dataset.sffHiddenBy === 'sff') {
hiddenSectionsCount++;
}
});
return hiddenSectionsCount;
},
filterActivities() {
const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]');
if (!settings.enabled) {
activities.forEach(activity => {
activity.style.display = '';
});
// Still count hidden sections even when activity filtering is disabled
const hiddenSectionsCount = this.countHiddenSections();
const btn = document.querySelector('.sff-clean-btn .sff-btn-sub');
if (btn) btn.textContent = `(${hiddenSectionsCount})`;
return;
}
let hiddenCount = 0;
activities.forEach(activity => {
const ownerLink = activity.querySelector('.entry-athlete a, [data-testid="owners-name"]');
// Handle club posts
if (ownerLink && ownerLink.getAttribute('href')?.includes('/clubs/')) {
if (settings.hideClubPosts) {
activity.style.display = 'none';
hiddenCount++;
} else {
// Ensure previously hidden club posts are shown again when toggled off
activity.style.display = '';
}
return; // Club posts are not subject to other filters
}
// Handle joined challenge cards separately
if (this.isChallengeEntry(activity)) {
if (settings.hideJoinedChallenges) {
activity.style.display = 'none';
hiddenCount++;
} else {
activity.style.display = '';
}
return; // Do not apply activity filters to challenge cards
}
const title = activity.querySelector('.entry-title, .activity-name, [data-testid="entry-title"], [data-testid="activity_name"]')?.textContent || '';
const athleteName = ownerLink?.textContent || '';
const svgIcon = activity.querySelector('svg[data-testid="activity-icon"] title');
const typeEl = activity.querySelector('[data-testid="tag"]') || activity.querySelector('.entry-head, .activity-type');
const type = svgIcon?.textContent || typeEl?.textContent || '';
let shouldHide = false;
// Keywords, Activity types, Distance, Duration, Elevation, Pace, Map, Athletes logic...
// [Filtering logic implementation here]
if (!shouldHide && settings.keywords.length > 0 && title) {
const hasKeyword = settings.keywords.some(keyword => keyword && title.toLowerCase().includes(keyword.toLowerCase()));
if (hasKeyword) shouldHide = true;
}
if (!shouldHide && type) {
const typeLower = type.toLowerCase();
const matched = TYPES.find(t => typeLower.includes(t.label.toLowerCase()));
if (matched && settings.types[matched.key]) {
shouldHide = true;
} else if (typeLower.includes('virtual')) {
const hideAnyVirtual = TYPES.filter(t => t.label.toLowerCase().includes('virtual')).some(t => settings.types[t.key]);
if (hideAnyVirtual) shouldHide = true;
}
}
if (!shouldHide && (settings.minKm > 0 || settings.maxKm > 0)) {
const km = UtilsModule.parseDistanceKm(activity);
if (km !== null) {
const val = settings.unitSystem === 'metric' ? km : km * 0.621371;
if (settings.minKm > 0 && val < settings.minKm) shouldHide = true;
if (!shouldHide && settings.maxKm > 0 && val > settings.maxKm) shouldHide = true;
}
}
if (!shouldHide && (settings.minMins > 0 || settings.maxMins > 0)) {
const secs = UtilsModule.parseDurationSeconds(activity);
if (secs !== null) {
const mins = secs / 60;
if (settings.minMins > 0 && mins < settings.minMins) shouldHide = true;
if (!shouldHide && settings.maxMins > 0 && mins > settings.maxMins) shouldHide = true;
}
}
if (!shouldHide && (settings.minElevM > 0 || settings.maxElevM > 0)) {
const elevM = UtilsModule.parseElevationM(activity);
if (elevM !== null) {
const val = settings.unitSystem === 'metric' ? elevM : elevM * 3.28084;
if (settings.minElevM > 0 && val < settings.minElevM) shouldHide = true;
if (!shouldHide && settings.maxElevM > 0 && val > settings.maxElevM) shouldHide = true;
}
}
if (!shouldHide && (settings.minPace > 0 || settings.maxPace > 0) && type && type.toLowerCase().includes('run')) {
const paceEl = activity.querySelector('.pace .value, [data-testid="pace"] .value');
if (paceEl) {
const paceText = paceEl.textContent || '';
const paceParts = paceText.split(':').map(Number);
if (paceParts.length === 2 && !isNaN(paceParts[0]) && !isNaN(paceParts[1])) {
const paceInMinutes = paceParts[0] + paceParts[1] / 60;
const km = UtilsModule.parseDistanceKm(activity);
if (km !== null && km > 0) {
const pacePerKm = paceInMinutes / km;
const paceVal = settings.unitSystem === 'metric' ? pacePerKm : pacePerKm * 1.60934;
if (settings.minPace > 0 && paceVal < settings.minPace) shouldHide = true;
if (!shouldHide && settings.maxPace > 0 && paceVal > settings.maxPace) shouldHide = true;
}
}
}
}
if (!shouldHide && settings.hideNoMap) {
// Only apply no-map rule to real activity entries
if (this.isActivityEntry(activity)) {
const map = activity.querySelector('img[data-testid="map"], svg.map, .activity-map, [data-testid="activity-map"]');
if (!map) shouldHide = true;
}
}
if (shouldHide && settings.allowedAthletes.length > 0 && athleteName) {
const nameParts = athleteName.toLowerCase().split(/\s+/);
const isAllowed = settings.allowedAthletes.some(allowedName => {
if (!allowedName) return false;
const allowedNameParts = allowedName.toLowerCase().split(/\s+/);
return allowedNameParts.every(part => nameParts.includes(part));
});
if (isAllowed) {
shouldHide = false;
}
}
if (shouldHide) {
activity.style.display = 'none';
hiddenCount++;
} else {
activity.style.display = '';
}
});
console.log(`🎯 Filtered ${hiddenCount}/${activities.length} activities`);
const btn = document.querySelector('.sff-clean-btn .sff-btn-sub');
const secondaryBtn = document.querySelector('.sff-secondary-filter-btn .sff-btn-sub');
if (btn) btn.textContent = `(${hiddenCount})`;
if (secondaryBtn) secondaryBtn.textContent = `(${hiddenCount})`;
},
manageHeaderKudosButton() {
let attempts = 0;
const maxAttempts = 10;
const interval = 500;
const placeButton = () => {
const kudosListItem = document.getElementById('gj-kudos-li');
if (!settings.enabled || !settings.showKudosButton) {
if (kudosListItem) kudosListItem.remove();
// Also ensure secondary button is hidden
UIModule.syncSecondaryKudosVisibility();
return;
}
if (kudosListItem) {
// Button exists, ensure secondary is also synced
UIModule.syncSecondaryKudosVisibility();
return;
}
const navList = document.querySelector('.user-nav.nav-group');
if (navList) {
const newListItem = document.createElement('li');
newListItem.id = 'gj-kudos-li';
newListItem.className = 'nav-item';
newListItem.dataset.addedByScript = 'true';
newListItem.style.paddingRight = '10px';
newListItem.style.display = 'flex';
newListItem.style.alignItems = 'center';
const kudosBtn = document.createElement('a');
kudosBtn.className = 'sff-header-kudos-btn';
kudosBtn.href = 'javascript:void(0);';
kudosBtn.textContent = 'Give 👍 to Everyone';
kudosBtn.addEventListener('click', () => {
let kudosGiven = 0;
const kudosButtons = document.querySelectorAll("button[data-testid='kudos_button']");
kudosButtons.forEach(button => {
const feedEntry = button.closest('.activity, .feed-entry, [data-testid="web-feed-entry"]');
if (feedEntry && feedEntry.style.display !== 'none' && button.title !== 'View all kudos') {
button.click();
kudosGiven++;
}
});
const originalText = kudosBtn.textContent;
kudosBtn.textContent = `Gave ${kudosGiven} 👍`;
kudosBtn.style.pointerEvents = 'none';
setTimeout(() => {
kudosBtn.textContent = originalText;
kudosBtn.style.pointerEvents = 'auto';
}, 3000);
});
newListItem.appendChild(kudosBtn);
navList.prepend(newListItem);
// Sync secondary button visibility after creating main button
UIModule.syncSecondaryKudosVisibility();
} else {
attempts++;
if (attempts < maxAttempts) {
setTimeout(placeButton, interval);
}
}
};
placeButton();
},
setupAutoFilter() {
const debouncedFilter = UtilsModule.debounce(() => {
try {
this.filterActivities();
this.updateGiftVisibility();
this.updateChallengesVisibility();
this.updateSuggestedFriendsVisibility();
this.updateYourClubsVisibility();
this.updateFooterVisibility();
this.updateJoinedChallengesVisibility();
this.updateMyWindsockVisibility();
this.updateSummitbagVisibility();
this.updateRunHealthVisibility();
} catch (e) {
console.error('Auto-filter error:', e);
}
}, 250);
this.filterActivities();
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (!m.addedNodes || m.addedNodes.length === 0) continue;
for (const node of m.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (
(node.matches && node.matches('.activity, .feed-entry, [data-testid="web-feed-entry"]')) ||
node.querySelector?.('.activity, .feed-entry, [data-testid="web-feed-entry"]')
) {
debouncedFilter();
break;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener('scroll', debouncedFilter, { passive: true });
window.__sffObserver = observer;
},
// Master function to apply all filters (activities and sections) based on enabled state
applyAllFilters() {
if (settings.enabled) {
// When filter is enabled, apply all filtering
this.filterActivities();
this.updateGiftVisibility();
this.updateChallengesVisibility();
this.updateFooterVisibility();
this.updateJoinedChallengesVisibility();
this.updateSuggestedFriendsVisibility();
this.updateYourClubsVisibility();
this.updateMyWindsockVisibility();
this.updateSummitbagVisibility();
this.updateRunHealthVisibility();
this.manageHeaderKudosButton();
UIModule.syncSecondaryKudosVisibility();
} else {
// When filter is disabled, show all activities and reset sections
const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]');
activities.forEach(activity => {
activity.style.display = '';
});
// Reset all sections to visible
const challengesSection = document.querySelector('#your-challenges');
if (challengesSection && challengesSection.dataset.sffHiddenBy === 'sff') {
challengesSection.style.display = '';
delete challengesSection.dataset.sffHiddenBy;
}
const suggestedFriendsSection = document.querySelector('#suggested-follows');
if (suggestedFriendsSection && suggestedFriendsSection.dataset.sffHiddenBy === 'sff') {
suggestedFriendsSection.style.display = '';
delete suggestedFriendsSection.dataset.sffHiddenBy;
}
const yourClubsSection = document.querySelector('#your-clubs');
if (yourClubsSection && yourClubsSection.dataset.sffHiddenBy === 'sff') {
yourClubsSection.style.display = '';
delete yourClubsSection.dataset.sffHiddenBy;
}
const giftLinks = document.querySelectorAll('a[href*="/gift"][href*="origin=global_nav"]');
giftLinks.forEach(a => {
if (a.dataset.sffHiddenBy === 'sff') {
a.style.display = '';
delete a.dataset.sffHiddenBy;
}
});
// Reset footer visibility
this.updateFooterVisibility();
// Reset joined challenges
const entries = document.querySelectorAll('[data-testid="web-feed-entry"], .feed-entry, .activity');
entries.forEach(entry => {
if (entry.dataset.sffHiddenChallenge === 'sff') {
entry.style.display = '';
delete entry.dataset.sffHiddenChallenge;
}
});
// Reset external service embed activities
this.updateMyWindsockVisibility();
this.updateSummitbagVisibility();
this.updateRunHealthVisibility();
// Hide kudos buttons when master toggle is off
this.manageHeaderKudosButton();
UIModule.syncSecondaryKudosVisibility();
// Update button counter to 0
const btn = document.querySelector('.sff-clean-btn .sff-btn-sub');
const secondaryBtn = document.querySelector('.sff-secondary-filter-btn .sff-btn-sub');
if (btn) btn.textContent = '(0)';
if (secondaryBtn) secondaryBtn.textContent = '(0)';
}
}
};
// Initialize utilities and update settings references
let settings = UtilsModule.loadSettings();
function handleClickOutside(event, panel, btn) {
// Check if click is outside panel and not on the toggle button
if (!panel.contains(event.target) && !btn.contains(event.target)) {
panel.classList.remove('show');
panel.style.display = 'none';
document.removeEventListener('click', (e) => handleClickOutside(e, panel, btn));
}
}
function setupEvents(btn, panel, secondaryFilterBtn, secondaryKudosBtn) {
console.log('🎯 Clean Filter: Setting up events...');
// Initialize draggable
const cleanupDraggable = makeDraggable(panel);
// Load saved position
const savedPos = JSON.parse(localStorage.getItem('sffPanelPos') || '{}');
if (savedPos.left || savedPos.top) {
panel.style.left = savedPos.left || '';
panel.style.top = savedPos.top || '';
panel.style.right = savedPos.left ? 'auto' : '10px';
}
// Ensure panel is in viewport on load
setTimeout(() => keepInViewport(panel), 100);
// Handle window resize
let resizeTimeout;
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const wasVisible = panel.style.display === 'block';
if (wasVisible) {
panel.style.display = 'none';
}
// Force reflow to ensure proper measurements
void panel.offsetHeight;
// Update position to stay in viewport
keepInViewport(panel);
if (wasVisible) {
panel.style.display = 'block';
}
// Save new position
localStorage.setItem('sffPanelPos', JSON.stringify({
left: panel.style.left,
top: panel.style.top
}));
}, 100);
};
window.addEventListener('resize', handleResize);
// Handle click outside (define first)
const handleClickOutside = (e) => {
const clickedSecondaryBtn = secondaryFilterBtn && secondaryFilterBtn.contains(e.target);
if (!panel.contains(e.target) && !btn.contains(e.target) && !clickedSecondaryBtn) {
const isVisible = panel.style.display === 'block' && panel.style.visibility !== 'hidden';
if (isVisible) {
togglePanel();
}
}
};
// Toggle panel function
const togglePanel = () => {
const isVisible = panel.style.display === 'block' && panel.style.visibility !== 'hidden';
console.log('🔄 Toggle panel called. Currently visible:', isVisible);
if (!isVisible) {
console.log('📁 Showing panel...');
// Close all dropdowns before showing the panel
panel.querySelectorAll('.sff-dropdown.open').forEach(dropdown => {
dropdown.classList.remove('open');
const content = dropdown.querySelector('.sff-dropdown-content');
if (content) content.style.display = 'none';
});
// Position panel directly under the active button
const activeBtn = (window.innerWidth <= 1479 && secondaryFilterBtn) ? secondaryFilterBtn : btn;
const btnRect = activeBtn.getBoundingClientRect();
const gap = 5; // Small gap between button and panel
panel.style.left = btnRect.left + 'px';
panel.style.top = (btnRect.bottom + gap) + 'px';
panel.style.right = 'auto';
// Show panel
panel.style.display = 'block';
panel.style.visibility = 'visible';
panel.style.opacity = '1';
panel.classList.add('show');
// Ensure panel stays within viewport after positioning
keepInViewport(panel);
console.log('✅ Panel should now be visible');
// Add click outside handler
setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 0);
} else {
console.log('🚫 Hiding panel...');
// Hide panel
panel.classList.remove('show');
panel.style.opacity = '0';
panel.style.visibility = 'hidden';
document.removeEventListener('click', handleClickOutside);
// After transition completes, update display
setTimeout(() => {
if (panel.style.visibility === 'hidden') {
panel.style.display = 'none';
}
}, 200);
}
};
// Toggle panel on button click
btn.addEventListener('click', (e) => {
console.log('🔥 Filter button clicked!');
e.stopPropagation();
togglePanel();
});
// Setup secondary filter button event (only if exists)
if (secondaryFilterBtn) {
secondaryFilterBtn.addEventListener('click', (e) => {
console.log('🔥 Secondary filter button clicked!');
e.stopPropagation();
togglePanel();
});
}
// Setup secondary kudos button event (only if exists)
if (secondaryKudosBtn) {
secondaryKudosBtn.addEventListener('click', () => {
let kudosGiven = 0;
const kudosButtons = document.querySelectorAll("button[data-testid='kudos_button']");
kudosButtons.forEach(button => {
const feedEntry = button.closest('.activity, .feed-entry, [data-testid="web-feed-entry"]');
if (feedEntry && feedEntry.style.display !== 'none' && button.title !== 'View all kudos') {
button.click();
kudosGiven++;
}
});
const originalText = secondaryKudosBtn.textContent;
secondaryKudosBtn.textContent = `Gave ${kudosGiven} 👍`;
secondaryKudosBtn.style.pointerEvents = 'none';
setTimeout(() => {
secondaryKudosBtn.textContent = originalText;
secondaryKudosBtn.style.pointerEvents = 'auto';
}, 3000);
});
}
// Close button
panel.querySelector('.sff-close').addEventListener('click', (e) => {
e.stopPropagation();
togglePanel();
});
// Main toggle switch
panel.querySelector('.sff-enabled-toggle').addEventListener('change', (e) => {
settings.enabled = e.target.checked;
UtilsModule.saveSettings(settings);
LogicModule.applyAllFilters();
});
// Toggle all dropdowns
panel.querySelectorAll('.sff-dropdown-header').forEach(header => {
header.addEventListener('click', (e) => {
const dropdown = e.currentTarget.closest('.sff-dropdown');
if (!dropdown) return;
const content = dropdown.querySelector('.sff-dropdown-content');
const isVisible = content.style.display === 'block';
content.style.display = isVisible ? 'none' : 'block';
dropdown.classList.toggle('open', !isVisible);
});
});
// Dragging - Only use makeDraggable from setupEvents, not setupDragging
// setupDragging(panel); // Remove duplicate dragging logic
setupWindowResize(panel);
setupButtonResponsive(btn);
UIModule.updateActivityCount(panel);
UIModule.updateFilterLabels(panel, settings.unitSystem);
// Unit system toggle
panel.querySelector('.sff-unit-toggle').addEventListener('click', (e) => {
if (e.target.matches('.sff-unit-btn')) {
const newUnit = e.target.dataset.unit;
if (newUnit !== settings.unitSystem) {
settings.unitSystem = newUnit;
panel.querySelectorAll('.sff-unit-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
UIModule.updateFilterLabels(panel, newUnit);
}
}
});
console.log('✅ Events attached');
// Return cleanup function for when the script is unloaded
return () => {
window.removeEventListener('resize', handleResize);
cleanupDraggable && cleanupDraggable();
document.removeEventListener('click', handleClickOutside);
};
}
function setupDragging(panel) {
const header = panel.querySelector('.sff-panel-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(window.getComputedStyle(panel).right, 10);
startTop = parseInt(window.getComputedStyle(panel).top, 10);
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = startX - e.clientX;
const deltaY = e.clientY - startY;
const newRight = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startLeft + deltaX));
const newTop = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startTop + deltaY));
panel.style.right = newRight + 'px';
panel.style.top = newTop + 'px';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
localStorage.setItem(POS_KEY, JSON.stringify({
top: panel.style.top,
right: panel.style.right
}));
}
isDragging = false;
});
}
// Hide/show the Strava header "Give a Gift" button based on settings
function updateGiftVisibility() {
try {
const links = document.querySelectorAll('a[href*="/gift"][href*="origin=global_nav"]');
links.forEach(a => {
if (settings.hideGiveGift) {
if (a.dataset.sffHiddenBy !== 'sff') {
a.dataset.sffHiddenBy = 'sff';
a.style.display = 'none';
}
} else if (a.dataset.sffHiddenBy === 'sff') {
a.style.display = '';
delete a.dataset.sffHiddenBy;
}
});
} catch (e) {
console.warn('updateGiftVisibility error:', e);
}
}
function filterActivities() {
const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]');
if (!settings.enabled) {
activities.forEach(activity => {
activity.style.display = '';
});
const btn = document.querySelector('.sff-clean-btn .sff-btn-sub');
if (btn) btn.textContent = '(0)';
return;
}
let hiddenCount = 0;
activities.forEach(activity => {
const ownerLink = activity.querySelector('.entry-athlete a, [data-testid="owners-name"]');
// Handle club posts
if (ownerLink && ownerLink.getAttribute('href')?.includes('/clubs/')) {
if (settings.hideClubPosts) {
activity.style.display = 'none';
hiddenCount++;
}
return; // Club posts are not subject to other filters
}
const title = activity.querySelector('.entry-title, .activity-name, [data-testid="entry-title"], [data-testid="activity_name"]')?.textContent || '';
const athleteName = ownerLink?.textContent || '';
const svgIcon = activity.querySelector('svg[data-testid="activity-icon"] title');
const typeEl = activity.querySelector('[data-testid="tag"]') || activity.querySelector('.entry-head, .activity-type');
const type = svgIcon?.textContent || typeEl?.textContent || '';
let shouldHide = false;
// Keywords
if (!shouldHide && settings.keywords.length > 0 && title) {
const hasKeyword = settings.keywords.some(keyword => keyword && title.toLowerCase().includes(keyword.toLowerCase()));
if (hasKeyword) shouldHide = true;
}
// Activity types
if (!shouldHide && type) {
const typeLower = type.toLowerCase();
const matched = TYPES.find(t => typeLower.includes(t.label.toLowerCase()));
if (matched && settings.types[matched.key]) {
shouldHide = true;
} else if (typeLower.includes('virtual')) {
const hideAnyVirtual = TYPES.filter(t => t.label.toLowerCase().includes('virtual')).some(t => settings.types[t.key]);
if (hideAnyVirtual) shouldHide = true;
}
}
// Distance
if (!shouldHide && (settings.minKm > 0 || settings.maxKm > 0)) {
const km = UtilsModule.parseDistanceKm(activity);
if (km !== null) {
const val = settings.unitSystem === 'metric' ? km : km * 0.621371;
if (settings.minKm > 0 && val < settings.minKm) shouldHide = true;
if (!shouldHide && settings.maxKm > 0 && val > settings.maxKm) shouldHide = true;
}
}
// Duration (minutes)
if (!shouldHide && (settings.minMins > 0 || settings.maxMins > 0)) {
const secs = UtilsModule.parseDurationSeconds(activity);
if (secs !== null) {
const mins = secs / 60;
if (settings.minMins > 0 && mins < settings.minMins) shouldHide = true;
if (!shouldHide && settings.maxMins > 0 && mins > settings.maxMins) shouldHide = true;
}
}
// Elevation Gain
if (!shouldHide && (settings.minElevM > 0 || settings.maxElevM > 0)) {
const elevM = UtilsModule.parseElevationM(activity);
if (elevM !== null) {
const val = settings.unitSystem === 'metric' ? elevM : elevM * 3.28084;
if (settings.minElevM > 0 && val < settings.minElevM) shouldHide = true;
if (!shouldHide && settings.maxElevM > 0 && val > settings.maxElevM) shouldHide = true;
}
}
// Pace for runs
if (!shouldHide && (settings.minPace > 0 || settings.maxPace > 0) && type && type.toLowerCase().includes('run')) {
const paceEl = activity.querySelector('.pace .value, [data-testid="pace"] .value');
if (paceEl) {
const paceText = paceEl.textContent || '';
const paceParts = paceText.split(':').map(Number);
if (paceParts.length === 2 && !isNaN(paceParts[0]) && !isNaN(paceParts[1])) {
const paceInMinutes = paceParts[0] + paceParts[1] / 60;
const km = UtilsModule.parseDistanceKm(activity);
if (km !== null && km > 0) {
const pacePerKm = paceInMinutes / km;
const paceVal = settings.unitSystem === 'metric' ? pacePerKm : pacePerKm * 1.60934;
if (settings.minPace > 0 && paceVal < settings.minPace) shouldHide = true; // Faster than min
if (!shouldHide && settings.maxPace > 0 && paceVal > settings.maxPace) shouldHide = true; // Slower than max
}
}
}
}
// Hide activities without a map
if (!shouldHide && settings.hideNoMap) {
const map = activity.querySelector('img[data-testid="map"], svg.map, .activity-map, [data-testid="activity-map"]');
if (!map) shouldHide = true;
}
// Allowed Athletes override
if (shouldHide && settings.allowedAthletes.length > 0 && athleteName) {
const nameParts = athleteName.toLowerCase().split(/\s+/);
const isAllowed = settings.allowedAthletes.some(allowedName => {
if (!allowedName) return false;
const allowedNameParts = allowedName.toLowerCase().split(/\s+/);
return allowedNameParts.every(part => nameParts.includes(part));
});
if (isAllowed) {
shouldHide = false; // It's an allowed athlete, so don't hide it
}
}
if (shouldHide) {
activity.style.display = 'none';
hiddenCount++;
} else {
activity.style.display = '';
}
});
console.log(`🎯 Filtered ${hiddenCount}/${activities.length} activities`);
// Add hidden sections count to the total
const hiddenSectionsCount = this.countHiddenSections();
const totalHiddenCount = hiddenCount + hiddenSectionsCount;
const btn = document.querySelector('.sff-clean-btn .sff-btn-sub');
const secondaryBtn = document.querySelector('.sff-secondary-filter-btn .sff-btn-sub');
if (btn) btn.textContent = `(${totalHiddenCount})`;
if (secondaryBtn) secondaryBtn.textContent = `(${totalHiddenCount})`;
}
function manageHeaderKudosButton() {
let attempts = 0;
const maxAttempts = 10; // Try for 5 seconds
const interval = 500; // 0.5 seconds
const placeButton = () => {
const kudosListItem = document.getElementById('gj-kudos-li');
// If button should be hidden, remove it and stop.
if (!settings.showKudosButton) {
if (kudosListItem) kudosListItem.remove();
// Also ensure secondary button is hidden
UIModule.syncSecondaryKudosVisibility();
return;
}
// If button already exists, ensure secondary is synced
if (kudosListItem) {
UIModule.syncSecondaryKudosVisibility();
return;
}
const navList = document.querySelector('.user-nav.nav-group');
if (navList) {
const newListItem = document.createElement('li');
newListItem.id = 'gj-kudos-li';
newListItem.className = 'nav-item';
newListItem.dataset.addedByScript = 'true'; // Mark for cleanup
newListItem.style.paddingRight = '10px';
newListItem.style.display = 'flex';
newListItem.style.alignItems = 'center';
const kudosBtn = document.createElement('a');
kudosBtn.className = 'sff-header-kudos-btn';
kudosBtn.href = 'javascript:void(0);';
kudosBtn.textContent = 'Give 👍 to Everyone';
kudosBtn.addEventListener('click', () => {
let kudosGiven = 0;
const kudosButtons = document.querySelectorAll("button[data-testid='kudos_button']");
kudosButtons.forEach(button => {
const feedEntry = button.closest('.activity, .feed-entry, [data-testid="web-feed-entry"]');
if (feedEntry && feedEntry.style.display !== 'none' && button.title !== 'View all kudos') {
button.click();
kudosGiven++;
}
});
const originalText = kudosBtn.textContent;
kudosBtn.textContent = `Gave ${kudosGiven} 👍`;
kudosBtn.style.pointerEvents = 'none';
setTimeout(() => {
kudosBtn.textContent = originalText;
kudosBtn.style.pointerEvents = 'auto';
}, 3000);
});
newListItem.appendChild(kudosBtn);
navList.prepend(newListItem);
// Sync secondary button visibility after creating main button
UIModule.syncSecondaryKudosVisibility();
} else {
attempts++;
if (attempts < maxAttempts) {
setTimeout(placeButton, interval);
}
}
};
placeButton();
}
// Observe DOM for new activities and re-apply filters automatically
function setupAutoFilter() {
const debouncedFilter = UtilsModule.debounce(() => {
try {
LogicModule.filterActivities();
LogicModule.updateGiftVisibility();
LogicModule.updateChallengesVisibility();
LogicModule.updateSuggestedFriendsVisibility();
LogicModule.updateYourClubsVisibility();
} catch (e) {
console.error('Auto-filter error:', e);
}
}, 250);
// Initial filter
filterActivities();
// MutationObserver for dynamically inserted feed entries
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (!m.addedNodes || m.addedNodes.length === 0) continue;
for (const node of m.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
// If the added node is an activity or contains one, trigger filtering
if (
(node.matches && node.matches('.activity, .feed-entry, [data-testid="web-feed-entry"]')) ||
node.querySelector?.('.activity, .feed-entry, [data-testid="web-feed-entry"]')
) {
debouncedFilter();
break;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Fallback: when user scrolls and Strava lazy-loads content, re-run filtering
window.addEventListener('scroll', debouncedFilter, { passive: true });
// Store on window for potential debugging/cleanup
window.__sffObserver = observer;
}
// ==== SFF SECTION: INIT BOOTSTRAP ====
// Setup global features that work on all pages
let globalFeaturesInitialized = false;
function setupGlobalFeatures() {
if (globalFeaturesInitialized) return;
globalFeaturesInitialized = true;
// Apply gift button hiding immediately
LogicModule.updateGiftVisibility();
// Apply challenges section hiding immediately
LogicModule.updateChallengesVisibility();
// Apply suggested friends section hiding immediately
LogicModule.updateSuggestedFriendsVisibility();
// Apply your clubs section hiding immediately
LogicModule.updateYourClubsVisibility();
// Apply footer hiding immediately
LogicModule.updateFooterVisibility();
// Apply external service embed hiding immediately
LogicModule.updateMyWindsockVisibility();
LogicModule.updateSummitbagVisibility();
LogicModule.updateRunHealthVisibility();
// Setup observer for dynamically loaded content to hide gift buttons and challenges
const observer = new MutationObserver(() => {
LogicModule.updateGiftVisibility();
LogicModule.updateChallengesVisibility();
LogicModule.updateFooterVisibility();
LogicModule.updateJoinedChallengesVisibility();
LogicModule.updateSuggestedFriendsVisibility();
LogicModule.updateYourClubsVisibility();
LogicModule.updateMyWindsockVisibility();
LogicModule.updateSummitbagVisibility();
LogicModule.updateRunHealthVisibility();
});
observer.observe(document.body, { childList: true, subtree: true });
// Store observer for cleanup if needed
window.__sffGlobalObserver = observer;
}
// Initialize
function init() {
console.log('🚀 Clean Filter: Initializing...');
setTimeout(() => {
// Always setup global features on all pages
setupGlobalFeatures();
// Only create UI elements and run filtering on dashboard
if (UtilsModule.isOnDashboard()) {
UIModule.createElements();
LogicModule.manageHeaderKudosButton();
// Ensure secondary kudos button is properly synchronized
UIModule.syncSecondaryKudosVisibility();
if (settings.enabled) {
LogicModule.filterActivities();
LogicModule.setupAutoFilter();
}
}
}, 1500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Handle navigation changes in SPA
let currentPath = window.location.pathname;
const checkPageChange = () => {
if (window.location.pathname !== currentPath) {
currentPath = window.location.pathname;
if (UtilsModule.isOnDashboard()) {
// We navigated to dashboard, initialize dashboard-specific features
document.body.setAttribute('data-sff-dashboard', 'true');
if (!document.querySelector('.sff-secondary-nav')) {
// Create dashboard elements if they don't exist
const existingElements = document.querySelectorAll('.sff-clean-btn, .sff-clean-panel');
if (existingElements.length === 0) {
init();
}
}
// Ensure secondary kudos button visibility is synchronized after navigation
setTimeout(() => UIModule.syncSecondaryKudosVisibility(), 100);
} else {
// We navigated away from dashboard, cleanup dashboard-specific elements
document.body.removeAttribute('data-sff-dashboard');
document.querySelectorAll('.sff-clean-btn, .sff-clean-panel, .sff-secondary-nav').forEach(el => el.remove());
// Remove header kudos button if it exists
const kudosLi = document.getElementById('gj-kudos-li');
if (kudosLi && kudosLi.dataset.addedByScript) {
kudosLi.remove();
}
}
}
};
// Check for page changes periodically
setInterval(checkPageChange, 500);
console.log('✅ Clean Filter: Setup complete');
})();