// ==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 2.3.1-safari-ios // @license MIT // @author Inc21 // @match https://www.strava.com/* // @grant none // @run-at document-end // @noframes // @compatible safari // @homepageURL https://github.com/Inc21/Tempermonkey-Strava-Feed-Filter // @supportURL https://github.com/Inc21/Tempermonkey-Strava-Feed-Filter/issues // ==/UserScript== (function() { 'use strict'; /* * Copyright (c) 2025 Inc21 * Licensed under the MIT License. See LICENSE file in the project root for full license text. */ // DISCLAIMER: Currently tested with Firefox (Desktop), Firefox for Android, and Safari iOS. // Other browsers may work but are not yet fully verified for this release. console.log('🚀 Clean Filter: Script starting (Safari iOS compatible)...'); 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, hideWandrer: false, hideCommuteTag: false, hideJoinWorkout: false, hideCoachCat: false, hideAthleteJoinedClub: 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() { const style = document.createElement('style'); style.textContent = ` @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) and (min-width: 769px) { .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) and (min-width: 481px) { .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-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; /* Flexible on desktop */ 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; display: grid !important; grid-template-columns: 18px 1fr !important; align-items: start !important; gap: 6px !important; padding: 4px 0 !important; border: none !important; border-radius: 0 !important; line-height: 1.2 !important; background: transparent !important; cursor: pointer !important; transition: none !important; user-select: none !important; white-space: normal !important; overflow: visible !important; text-overflow: clip !important; min-width: 0 !important; word-break: break-word !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: 0 !important; margin-left: 0 !important; transform: scale(0.85) !important; align-self: center !important; justify-self: center !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; } `; document.head.appendChild(style); } // Initialize CSS Module injectStyles(); // Mobile Safari specific improvements function addMobileSafariSupport() { // Touch/click handling is attached explicitly to created buttons; no global overrides. // Do not modify page viewport to preserve accessibility and avoid iOS quirks. // Add iOS-specific CSS improvements const mobileStyle = document.createElement('style'); mobileStyle.textContent = ` @media (max-width: 768px) { .sff-clean-btn { touch-action: manipulation !important; -webkit-tap-highlight-color: transparent !important; -webkit-touch-callout: none !important; -webkit-user-select: none !important; user-select: none !important; font-size: 12px !important; padding: 8px 12px !important; } .sff-clean-panel { max-height: 90vh !important; width: 95vw !important; right: 2.5vw !important; left: auto !important; top: 50px !important; border-radius: 12px !important; } .sff-panel-content { max-height: calc(90vh - 80px) !important; -webkit-overflow-scrolling: touch !important; overflow-y: auto !important; padding: 12px !important; } .sff-panel-header { padding: 8px 12px !important; } .sff-panel-header h3 { font-size: 13px !important; } .sff-section { margin-bottom: 15px !important; padding-bottom: 10px !important; } .sff-section h4 { font-size: 12px !important; margin-bottom: 8px !important; } .sff-label { font-size: 13px !important; margin-bottom: 5px !important; } .sff-input { padding: 6px 10px !important; font-size: 13px !important; margin-bottom: 6px !important; } .sff-types { grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)) !important; gap: 3px 6px !important; } .sff-chip { font-size: 12px !important; padding: 3px 0 !important; } .sff-chip input { margin-right: 3px !important; transform: scale(0.9) !important; } .sff-input-group { grid-template-columns: 1fr !important; gap: 6px !important; } .sff-unit-btn { padding: 6px !important; font-size: 12px !important; } .sff-dropdown-header { padding: 6px 10px !important; font-size: 13px !important; } .sff-dropdown-content { padding: 8px !important; } .sff-activity-count { font-size: 11px !important; margin-top: 8px !important; padding-top: 8px !important; } } /* Extra small devices (phones in portrait) */ @media (max-width: 480px) { .sff-clean-panel { width: 98vw !important; right: 1vw !important; left: 1vw !important; max-height: 95vh !important; } .sff-panel-content { max-height: calc(95vh - 70px) !important; padding: 10px !important; } .sff-clean-btn { font-size: 11px !important; padding: 6px 10px !important; top: 5px !important; right: 5px !important; } .sff-types { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; gap: 2px 4px !important; } .sff-chip { font-size: 11px !important; } .sff-input { padding: 5px 8px !important; font-size: 12px !important; } } /* Landscape phones: 3 columns, avoid wrapping */ @media (orientation: landscape) and (max-height: 480px) { .sff-types { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; gap: 4px 6px !important; } .sff-clean-panel .sff-chip { display: flex !important; align-items: center !important; gap: 6px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } } /* iOS Safari specific fixes */ .sff-clean-btn { -webkit-appearance: none !important; appearance: none !important; } .sff-clean-panel input[type="text"], .sff-clean-panel input[type="number"], .sff-clean-panel textarea { -webkit-appearance: none !important; appearance: none !important; border-radius: 6px !important; } /* Ensure all content is visible and scrollable */ .sff-clean-panel { overflow: visible !important; } .sff-panel-content { overflow-y: auto !important; overflow-x: hidden !important; scrollbar-width: thin !important; -webkit-overflow-scrolling: touch !important; } /* Custom scrollbar for webkit browsers */ .sff-panel-content::-webkit-scrollbar { width: 6px !important; } .sff-panel-content::-webkit-scrollbar-track { background: #f1f1f1 !important; border-radius: 3px !important; } .sff-panel-content::-webkit-scrollbar-thumb { background: #fc5200 !important; border-radius: 3px !important; } .sff-panel-content::-webkit-scrollbar-thumb:hover { background: #e04a00 !important; } /* Ensure checkboxes are always visible */ .sff-chip input[type="checkbox"] { min-width: 16px !important; min-height: 16px !important; margin-right: 4px !important; flex-shrink: 0 !important; } /* Make sure dropdowns don't get cut off */ .sff-dropdown-content { position: relative !important; z-index: 1000 !important; } `; document.head.appendChild(mobileStyle); } // Initialize mobile Safari support addMobileSafariSupport(); // 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}; }, updateCommuteTagVisibility() { try { const activities = document.querySelectorAll('.activity, .feed-entry, [data-testid="web-feed-entry"]'); activities.forEach(activity => { const tags = Array.from(activity.querySelectorAll('[data-testid="tag"]')).map(el => (el.textContent || '').trim().toLowerCase()); const isCommute = tags.some(t => t === 'commute'); if (isCommute) { if (settings.enabled && settings.hideCommuteTag) { if (activity.dataset.sffHiddenCommute !== 'sff') { activity.dataset.sffHiddenCommute = 'sff'; activity.style.display = 'none'; } } else if (activity.dataset.sffHiddenCommute === 'sff') { activity.style.display = ''; delete activity.dataset.sffHiddenCommute; } } }); } catch (e) { console.warn('updateCommuteTagVisibility error:', e); } }, 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.hideWandrer = panel.querySelector('.sff-hideWandrer') ? panel.querySelector('.sff-hideWandrer').checked : settings.hideWandrer; settings.hideCommuteTag = panel.querySelector('.sff-hideCommuteTag') ? panel.querySelector('.sff-hideCommuteTag').checked : settings.hideCommuteTag; settings.hideJoinWorkout = panel.querySelector('.sff-hideJoinWorkout') ? panel.querySelector('.sff-hideJoinWorkout').checked : settings.hideJoinWorkout; settings.hideCoachCat = panel.querySelector('.sff-hideCoachCat') ? panel.querySelector('.sff-hideCoachCat').checked : settings.hideCoachCat; settings.hideAthleteJoinedClub = panel.querySelector('.sff-hideAthleteJoinedClub') ? panel.querySelector('.sff-hideAthleteJoinedClub').checked : settings.hideAthleteJoinedClub; 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, apply global and embed settings LogicModule.updateGiftVisibility(); LogicModule.updateChallengesVisibility(); LogicModule.updateSuggestedFriendsVisibility(); LogicModule.updateYourClubsVisibility(); LogicModule.updateMyWindsockVisibility(); LogicModule.updateSummitbagVisibility(); LogicModule.updateRunHealthVisibility(); LogicModule.updateJoinWorkoutVisibility(); LogicModule.updateCoachCatVisibility(); LogicModule.updateAthleteJoinedClubVisibility(); 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(POS_KEY) || '{}'); 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 = `
Adds a button to the header to give kudos to all visible activities.
Report a bug or dead filter: HERE