您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Injects RunRepeat reviews onto product pages of major shoe brands.
当前为
// ==UserScript== // @name RunRepeat Review Summaries on Shoe Sites // @namespace https://github.com/sinazadeh/userscripts // @version 1.1.0 // @description Injects RunRepeat reviews onto product pages of major shoe brands. // @author You // @match https://www.adidas.com/* // @match https://www.brooksrunning.com/* // @match https://www.hoka.com/* // @match https://www.on.com/* // @match https://www.newbalance.com/* // @match https://www.asics.com/* // @grant GM_xmlhttpRequest // @connect runrepeat.com // @license MIT // ==/UserScript== /* jshint esversion: 11 */ (function () { 'use strict'; let reviewData = null; let currentSlug = null; let currentConfig = null; let isFetching = false; let hasFailed = false; let lastUrl = location.href; const siteConfigs = { 'www.adidas.com': { brand: 'adidas', getSlug: () => document.querySelector('h1[data-testid="product-title"]')?.textContent.trim().toLowerCase().replace(/\s+/g, '-') || window.location.pathname.split('/').filter(Boolean)[1]?.toLowerCase() || null, injectionTarget: '[data-testid="buy-section"], .product-description', injectionMethod: 'after' }, 'www.brooksrunning.com': { brand: 'brooks', getSlug: () => document.querySelector('h1.m-buy-box-header__name')?.textContent.trim().toLowerCase().replace(/\s+/g, '-') || null, injectionTarget: '.m-buy-box .js-pdp-add-cart-btn', injectionMethod: 'after' }, 'www.hoka.com': { brand: 'hoka', getSlug: () => document.querySelector('h1[data-qa="productName"]')?.textContent.trim().toLowerCase().replace(/\s+/g, '-') || null, injectionTarget: 'div.product-primary-attributes', injectionMethod: 'after' }, 'www.on.com': { brand: 'on', getSlug: () => { const el = document.querySelector('h1[data-test-id="productNameTitle"]'); if (!el) return null; const clone = el.cloneNode(true); clone.querySelectorAll('span').forEach(span => span.remove()); return clone.textContent.trim().toLowerCase().replace(/\s+/g, '-'); }, injectionTarget: '[data-test-id="cartButton"]', injectionMethod: 'after' }, 'www.newbalance.com': { brand: 'new-balance', getSlug: () => { const el = document.querySelector('#productDetails h1, h1.product-name'); if (!el) return null; let txt = el.textContent.trim(); txt = txt.replace(/(\d)(v\d+)/gi, '$1 $2').replace(/([a-z])([A-Z])/g, '$1 $2'); return txt.toLowerCase().replace(/\s+/g, '-'); }, injectionTarget: '.prices-add-to-cart-actions', injectionMethod: 'after' }, 'www.asics.com': { brand: 'asics', getSlug: () => document.querySelector('h1.pdp-top__product-name__not-ot')?.textContent.trim().toLowerCase().replace(/\s+/g, '-') || null, injectionTarget: '.pdp-top__cta.product-add-to-cart', injectionMethod: 'after' } }; function generateRunRepeatURLs(slug, brand) { if (!slug) return []; const cleanSlug = slug.replace(/-shoes$/, ''); return [ `https://runrepeat.com/${brand}-${cleanSlug}`, `https://runrepeat.com/${brand}-${slug}` ]; } function fetchAndParseRunRepeat(url) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url, onload: (res) => { if (res.status !== 200) return resolve(null); const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); if (!doc.querySelector('#product-intro')) return resolve(null); console.log(`[RR Injector] SUCCESS: Found data at ${url}`); resolve({ ...parseRunRepeat(doc), url }); }, onerror: () => resolve(null) }); }); } async function findValidRunRepeatPage(slug, brand) { const urls = generateRunRepeatURLs(slug, brand); const results = await Promise.all(urls.map(fetchAndParseRunRepeat)); return results.find(Boolean) || null; } function parseRunRepeat(doc) { const q = (sel) => doc.querySelector(sel)?.textContent.trim() || ''; const scoreEl = doc.querySelector('#audience_verdict #corescore .corescore-big__score'); return { verdict: q('#product-intro .product-intro-verdict + div'), pros: [...doc.querySelectorAll('#the_good ul li')].map(li => li.textContent.trim()), cons: [...doc.querySelectorAll('#the_bad ul li')].map(li => li.textContent.trim()), audienceScore: parseInt(scoreEl?.textContent.trim() || '0', 10), scoreText: q('#audience_verdict .corescore-big__text'), awards: [...doc.querySelectorAll('#product-intro ul.awards-list li, #audience_verdict ul.awards-list li')].map(li => li.textContent.replace(/\s+/g, ' ').trim()) }; } function createRunRepeatSection(data) { const scoreColorMap = { 'superb': '#098040', 'great': '#098040', 'good': '#54cb62', 'decent': '#ffb717', 'bad': '#eb1c24' }; const scoreKey = (data.scoreText || '').replace('!', '').toLowerCase(); const scoreColor = scoreColorMap[scoreKey] || '#6c757d'; const section = document.createElement('div'); section.className = 'runrepeat-section'; section.style.cssText = `border:1px solid #e0e0e0; border-radius:8px; padding:20px; margin:20px 0; background:#fdfdfd; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;`; section.innerHTML = ` <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; padding-bottom:12px; border-bottom:2px solid #eee;"> <div style="display:flex; align-items:center; gap:12px;"> <div style="background:#000; color:white; padding:6px 12px; border-radius:4px; font-weight:bold; font-size:14px;">RunRepeat</div> <h3 style="margin:0; font-size:20px; font-weight:600; color:#111;">Expert Review</h3> </div> ${data.audienceScore ? `<div style="display:flex; align-items:center; gap:8px; background:white; padding:8px 16px; border-radius:20px; border:2px solid ${scoreColor};"><div style="font-size:24px; font-weight:bold; color:${scoreColor}; line-height:1;">${data.audienceScore}</div><div style="font-size:12px; font-weight:600; color:${scoreColor}; text-transform:uppercase;">${data.scoreText || ''}</div></div>` : ''} </div> ${renderAwards(data.awards)} <div style="margin-bottom:20px;"><h4 style="margin:0 0 10px 0; font-size:18px; color:#111; font-weight:600;">Expert Verdict</h4><div style="background:white; padding:16px; border-radius:6px; border-left:4px solid #007bff; font-size:16px; line-height:1.6; color:#333; box-shadow:0 1px 3px rgba(0,0,0,0.05);">${data.verdict || 'No verdict available.'}</div></div> ${buildListSection('👍 What\'s Great', data.pros, '#28a745')} ${buildListSection('👎 Consider This', data.cons, '#dc3545')} <div style="text-align:center; padding-top:20px; margin-top:20px; border-top:1px solid #eee;"><a href="${data.url}" target="_blank" style="color:#007bff; text-decoration:none; font-size:14px; font-weight:500;">Read the complete review on RunRepeat →</a></div>`; return section; } function renderAwards(awards) { if (!awards?.length) return ''; return `<div style="margin-bottom:20px;"><h4 style="margin:0 0 10px 0; font-size:14px; color:#555; text-transform:uppercase; letter-spacing:0.5px; font-weight:600;">Awards & Recognition</h4><div style="display:flex; flex-wrap:wrap; gap:8px;">${awards.map(award => `<span style="background:#fff8e1; color:#6d4c41; font-size:13px; font-weight:500; padding:6px 12px; border-radius:15px; border:1px solid #ffecb3;">🏆 ${award}</span>`).join('')}</div></div>`; } function buildListSection(title, items, color) { if (!items?.length) return ''; return `<div style="background:white; padding:20px; border-radius:8px; border-top:4px solid ${color}; box-shadow:0 2px 4px rgba(0,0,0,0.05); margin-bottom:16px;"><h4 style="margin:0 0 16px 0; font-size:16px; color:${color}; font-weight:600;">${title}</h4><ul style="margin:0; padding:0; list-style:none; color:#333;">${items.map(item => `<li style="font-size:14px; line-height:1.5; margin-bottom:10px; padding-left:20px; position:relative;"><span style="position:absolute; left:0; top:1px; color:${color};">${color === '#28a745' ? '✔' : '✘'}</span>${item}</li>`).join('')}</ul></div>`; } async function injectReviewSection() { if (hasFailed) return; currentConfig = siteConfigs[window.location.hostname]; if (!currentConfig) return; currentSlug = currentConfig.getSlug(); if (!currentSlug) { setTimeout(injectReviewSection, 500); return; } if (!reviewData && !isFetching) { isFetching = true; reviewData = await findValidRunRepeatPage(currentSlug, currentConfig.brand) || 'failed'; isFetching = false; } if (reviewData === 'failed') { hasFailed = true; return; } const target = document.querySelector(currentConfig.injectionTarget); if (!target) { setTimeout(injectReviewSection, 500); return; } if (!document.querySelector('.runrepeat-section')) { target.parentNode.insertBefore(createRunRepeatSection(reviewData), target.nextSibling); } } function handleUrlChange() { if (location.href !== lastUrl) { lastUrl = location.href; reviewData = null; hasFailed = false; document.querySelector('.runrepeat-section')?.remove(); debounceInject(); } } function hookHistoryEvents() { const pushState = history.pushState; history.pushState = function (...args) { pushState.apply(this, args); handleUrlChange(); }; window.addEventListener('popstate', handleUrlChange); } let injectTimeout; function debounceInject() { clearTimeout(injectTimeout); injectTimeout = setTimeout(injectReviewSection, 400); } const observer = new MutationObserver(() => { if (!document.querySelector('.runrepeat-section')) { debounceInject(); } }); observer.observe(document.body, { childList: true, subtree: true }); hookHistoryEvents(); injectReviewSection(); })();