您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
年份/版本/格式/大小徽章 + 动态高亮连线:鼠标悬停时仅突出显示同书多版本,其余淡化;增强纯数字版本识别。
// ==UserScript== // @name Anna's Archive 搜索结果增强器 v1.3 // @namespace http://tampermonkey.net/ // @version 1.3.0 // @license MIT // @description 年份/版本/格式/大小徽章 + 动态高亮连线:鼠标悬停时仅突出显示同书多版本,其余淡化;增强纯数字版本识别。 // @author Assistant // @match *://*.annas-archive.org/* // @match *://annas-archive.org/* // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; /**************************** * 1. CONFIGURATION ***************************/ const CONFIG = { PREFERRED_FORMATS: ['pdf'], NON_PREFERRED_FORMATS: ['epub', 'fb2', 'mobi', 'azw3', 'djvu', 'cbz', 'cbr', 'rar', 'zip'], CURRENT_YEAR: new Date().getFullYear(), MIN_YEAR: 1900, NEW_YEAR_THRESHOLD: 10, COLORS: { PREFERRED: '#22c55e', WARNING: '#ef4444', NEUTRAL: '#64748b', SIZE: '#14b8a6', YEAR_NEW: ['#3b82f6', '#1d4ed8'], YEAR_OLD: ['#d1d5db', '#6b7280'], VERSION_NEW: ['#8b5cf6', '#7c3aed'], VERSION_OLD: ['#e5e7eb', '#9ca3af'] }, CLUSTER_COLORS: [ '#60a5fa', '#34d399', '#fbbf24', '#f472b6', '#a78bfa', '#f87171', '#2dd4bf', '#facc15' ], TITLE_SIM_THRESHOLD: 0.8, TITLE_DIST_THRESHOLD: 5 }; /**************************** * 2. UTILITIES ***************************/ const clean = (t = '') => t.replace(/\s+/g, ' ').trim(); const normTitle = (t = '') => t.toLowerCase().replace(/[^\w\s]/g, '').trim(); function levenshtein(a, b) { const m = a.length, n = b.length; if (!m) return n; if (!n) return m; const v0 = Array.from({ length: n + 1 }, (_, i) => i); const v1 = new Array(n + 1); for (let i = 0; i < m; i++) { v1[0] = i + 1; for (let j = 0; j < n; j++) { const cost = a[i] === b[j] ? 0 : 1; v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost); } for (let j = 0; j <= n; j++) v0[j] = v1[j]; } return v1[n]; } function jaroWinkler(s1, s2) { if (s1 === s2) return 1; const len1 = s1.length, len2 = s2.length; if (!len1 || !len2) return 0; const range = Math.max(len1, len2) / 2 - 1; const s2Match = new Array(len2).fill(false); let matches = 0, transpositions = 0; for (let i = 0; i < len1; i++) { const start = Math.max(0, i - range); const end = Math.min(i + range + 1, len2); for (let j = start; j < end; j++) { if (!s2Match[j] && s1[i] === s2[j]) { s2Match[j] = true; matches++; break; } } } if (!matches) return 0; let k = 0; for (let i = 0; i < len1; i++) { if (s1[i] === s2[[...s2Match.keys()].find(j => s2Match[j] && j >= k)]) { k = [...s2Match.keys()].find(j => s2Match[j] && j >= k) + 1; } else transpositions++; } transpositions /= 2; let sim = (matches / len1 + matches / len2 + (matches - transpositions) / matches) / 3; let l = 0; while (l < 4 && s1[l] === s2[l]) l++; return sim + l * 0.1 * (1 - sim); } function extractYear(text) { const years = (text.match(/\b(19|20)\d{2}\b/g) || []).map(Number).filter(y => y >= CONFIG.MIN_YEAR && y <= CONFIG.CURRENT_YEAR); return years.length ? Math.max(...years) : null; } function extractVersion(text) { const ordMap = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5, sixth: 6, seventh: 7, eighth: 8, ninth: 9, tenth: 10 }; let m; if (m = text.match(/\b(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\s*(edition|ed\.?)/i)) return ordMap[m[1].toLowerCase()]; if (m = text.match(/\b(\d{1,3})(?:st|nd|rd|th)?\s*(edition|ed\.?|版)/i)) return m[1]; if (m = text.match(/v(?:er\.?|ersion)?\s*(\d+(?:\.\d+)*)/i)) return m[1]; if (m = text.match(/第\s*(\d+)\s*版/)) return m[1]; // fallback: lone digit 1‑20 near year comma or in parentheses if (m = text.match(/[\(,\-]\s*(\d{1,2})\s*(?:[\),]|,\s*\d{4})/)) { const n = parseInt(m[1]); if (n >= 1 && n <= 20) return n; } return null; } function extractFormats(text) { const set = new Set(); text.replace(/\b(pdf|epub|fb2|mobi|azw3|djvu|cbz|cbr|rar|zip)\b/gi, (_, f) => { set.add(f.toLowerCase()); return _; }); return Array.from(set); } function extractSize(text) { const m = text.match(/(\d+(?:\.\d+)?)\s*(kb|mb|gb)/i); return m ? `${m[1]}${m[2].toUpperCase()}` : null; } const grad = c => Array.isArray(c) ? `linear-gradient(135deg, ${c[0]}, ${c[1]})` : c; function badge(label, cls, color, tip='') { const s = document.createElement('span'); s.className = `aa-badge ${cls}`.trim(); s.textContent = label; s.style.background = grad(color); if (tip) s.title = tip; return s; } /**************************** * 3. STYLE ***************************/ function injectStyles() { if (document.getElementById('aa-style')) return; const css = ` .aa-container{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0;font-size:12px} .aa-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:12px;font-weight:600;color:#fff;line-height:1.2;text-shadow:0 1px 2px rgba(0,0,0,.25)} .aa-badge.format.preferred::before{content:'✔ ';font-weight:bold} .aa-badge.format.warning::before{content:'⚠ ';font-weight:bold} .aa-cluster-line{border-left:4px solid var(--cluster-color,transparent);padding-left:6px;opacity:.25;transition:opacity .2s ease} .aa-cluster-line.aa-active{opacity:1;} `; const style = document.createElement('style'); style.id='aa-style'; style.textContent = css; document.head.appendChild(style); } /**************************** * 4. RESULT ENHANCEMENT ***************************/ function enhanceResult(el) { if (el.querySelector('.aa-container')) return; const text = clean(el.textContent.toLowerCase()); const year = extractYear(text); const ver = extractVersion(text); const size = extractSize(text); const formats = extractFormats(text); const wrap = document.createElement('div'); wrap.className='aa-container'; if (year) { const isNew = CONFIG.CURRENT_YEAR - year <= CONFIG.NEW_YEAR_THRESHOLD; wrap.appendChild(badge(year,'year',isNew?CONFIG.COLORS.YEAR_NEW:CONFIG.COLORS.YEAR_OLD,`出版年份 ${year}`)); } if (ver){ const isLatest = parseFloat(ver)>=2; wrap.appendChild(badge(`v${ver}`,'version',isLatest?CONFIG.COLORS.VERSION_NEW:CONFIG.COLORS.VERSION_OLD,`版本 ${ver}`)); } if (size) wrap.appendChild(badge(size,'size',CONFIG.COLORS.SIZE,`文件大小 ${size}`)); formats.forEach(f=>{ let col=CONFIG.COLORS.NEUTRAL,cls='format',tip=`格式 ${f.toUpperCase()}`; if(CONFIG.PREFERRED_FORMATS.includes(f)){col=CONFIG.COLORS.PREFERRED;cls+=' preferred';tip='推荐格式 '+f.toUpperCase();} else if(CONFIG.NON_PREFERRED_FORMATS.includes(f)){col=CONFIG.COLORS.WARNING;cls+=' warning';tip='不推荐格式 '+f.toUpperCase();} wrap.appendChild(badge(f.toUpperCase(),cls,col,tip)); }); el.appendChild(wrap); } /**************************** * 5. CLUSTERING WITH DYNAMIC HOVER ***************************/ function clusterAndDecorate(items){ const clusters=[]; items.forEach(el=>{ const tNode=el.querySelector('h3,h2,.title,.bookTitle'); if(!tNode) return; const title=normTitle(tNode.textContent); let c=null; for(const cl of clusters){ if(jaroWinkler(title,cl.rep)>=CONFIG.TITLE_SIM_THRESHOLD||levenshtein(title,cl.rep)<=CONFIG.TITLE_DIST_THRESHOLD){c=cl;break;} } if(!c){ c={rep:title,items:[],color:CONFIG.CLUSTER_COLORS[clusters.length%CONFIG.CLUSTER_COLORS.length]}; clusters.push(c);} c.items.push(el); }); clusters.forEach((c,idx)=>{ if(c.items.length<2) return; c.items.forEach(el=>{ el.classList.add('aa-cluster-line'); el.style.setProperty('--cluster-color',c.color); el.dataset.aaCluster=idx; }); }); } function attachHoverLogic(){ document.addEventListener('mouseover',e=>{ const target=e.target.closest('[data-aa-cluster]'); document.querySelectorAll('.aa-cluster-line').forEach(el=>el.classList.remove('aa-active')); if(target){ const cluster=target.dataset.aaCluster; document.querySelectorAll(`[data-aa-cluster="${cluster}"]`).forEach(el=>el.classList.add('aa-active')); } }); } /**************************** * 6. FIND & ENHANCE ***************************/ function findResults(){ const sels=['[class*="result"]','[class*="item"]','[class*="book"]','[class*="entry"]','[class*="card"]','article','.search-result','.result-item','.book-item','[data-testid*="result"]']; let res=[]; sels.forEach(sel=>{const els=document.querySelectorAll(sel); if(els.length){ const f=[...els].filter(el=>{const t=el.textContent||''; return t.length>50&&(/[\.](pdf|epub|mobi)|MB|KB|GB|\d{4}/i.test(t));}); if(f.length>res.length) res=f;}}); return res; } function enhance(){ const items=findResults(); if(!items.length) return; items.forEach(enhanceResult); clusterAndDecorate(items); } /**************************** * 7. INIT ***************************/ function init(){ injectStyles(); enhance(); attachHoverLogic(); const obs=new MutationObserver(m=>{ if(m.some(x=>x.addedNodes.length)) setTimeout(enhance,300);}); obs.observe(document.body,{childList:true,subtree:true}); } document.readyState==='loading'?document.addEventListener('DOMContentLoaded',init):init(); })();