2048-预览

为2048核基地论坛帖子添加图片、链接预览

// ==UserScript==
// @name         2048-预览
// @version      1.1.0
// @namespace    https://sleazyfork.org/zh-CN/users/1461640-%E6%98%9F%E5%AE%BF%E8%80%81%E9%AD%94
// @author       星宿老魔
// @description  为2048核基地论坛帖子添加图片、链接预览
// @match        https://hjd2048.com/2048/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=hjd2048.com
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// ==/UserScript==

(function(){"use strict";const l={name:"2048-预览",version:"1.1.0",currentDomain:"hjd2048.com",defaults:{MAX_PREVIEW_IMAGES:3,CONCURRENT_LIMIT:9},get(o){try{return GM_getValue(o,this.defaults[o])}catch(t){return console.warn(`[${this.name}] 获取配置失败:`,t),this.defaults[o]}},set(o,t){try{GM_setValue(o,t)}catch(e){console.warn(`[${this.name}] 保存配置失败:`,e)}},getAll(){const o={};for(const t in this.defaults)o[t]=this.get(t);return o},selectors:{threadRows:"tr.tr3.t_one",threadLinks:'a[target="_self"], a[target="_blank"]',contentSelectors:["#read_tpc",".tpc_content",".f14.cc",'div[id="read_tpc"]',".t_f"],searchLink:'#nav-pc a[href="/search.php"]',navSearch:"#nav-s",imgSelectors:["#read_tpc img",".tpc_content img",".f14.cc img",'div[id="read_tpc"] img'],magnetTextarea:"textarea[readonly], textarea#copytext",magnetLink:'a[href^="magnet:?xt=urn:btih:"]',ed2kLink:'a[href^="ed2k://"]',btLink:'a[href*="bt.ivcbt.com/list.php?name="], a[href*="bt.bxmho.cn/list.php?name="]'},regex:{threadUrl:/read\.php\?tid=/,magnetHash:/([A-F0-9]{40})/i,fileSize:/【影片容量】:([^<]+)<br|【影片大小】:([^<]+)/i,thunder:/thunder:\/\/[A-Za-z0-9+\/=]+/i,ed2k:/ed2k:\/\/\|file\|[^|]+\|\d+\|[A-F0-9]{32}\|\//i,magnetLink:/magnet:\?xt=urn:btih:[a-zA-Z0-9]+/,copyText:/magnet:\?xt=urn:btih:/},filters:{badImagePatterns:[/^(none|empty|blank|default)\./,/^(icon|logo|banner|ad)_/,/\.(ico|cur)$/,/^(loading|wait|spinner)/],badImageClasses:["icon","emoji","smiley"],minImageSize:{width:100,height:100}}},h={copyToClipboard(o,t){navigator.clipboard.writeText(o).then(()=>{this.showClickTip("已复制",t)}).catch(()=>{console.warn("复制失败,使用fallback方法"),this.fallbackCopyTextToClipboard(o,t)})},fallbackCopyTextToClipboard(o,t){const e=document.createElement("textarea");e.value=o,e.style.position="fixed",e.style.top="-1000px",e.style.left="-1000px",document.body.appendChild(e),e.focus(),e.select();try{document.execCommand("copy"),this.showClickTip("已复制",t)}catch(i){console.error("复制失败:",i),this.showClickTip("复制失败",t)}document.body.removeChild(e)},showClickTip(o,t){const e=t;let i=document.querySelector(".click-tip");i&&i.remove(),i=document.createElement("div"),i.className="click-tip",i.textContent=o,document.body.appendChild(i),i.style.left=`${e.clientX}px`,i.style.top=`${e.clientY}px`,setTimeout(()=>{i.style.opacity="1"},10),setTimeout(()=>{i.style.opacity="0",setTimeout(()=>{i.parentElement&&i.remove()},200)},1e3)},removeRules(){try{const o=document.querySelector(".collapse-header");if(o){const t=o.closest("div, section, .rule-container, .collapse-container");t&&t.remove()}}catch(o){console.error("移除版规时出错:",o)}},isContentPage(){return l.regex.threadUrl.test(window.location.href)},async asyncPool(o,t,e){const i=[],n=[];for(const r of t){const a=Promise.resolve().then(()=>e(r,t));if(i.push(a),o<=t.length){const s=a.then(()=>n.splice(n.indexOf(s),1));n.push(s),n.length>=o&&await Promise.race(n)}}return Promise.all(i)},debounce(o,t,e){let i=null;return function(...n){const r=()=>{i=null,e||o.apply(this,n)},a=e&&!i;i&&clearTimeout(i),i=setTimeout(r,t),a&&o.apply(this,n)}},waitForElement(o,t=5e3){return new Promise(e=>{const i=document.querySelector(o);if(i){e(i);return}const n=new MutationObserver(()=>{const r=document.querySelector(o);r&&(n.disconnect(),e(r))});n.observe(document.body,{childList:!0,subtree:!0}),setTimeout(()=>{n.disconnect(),e(null)},t)})},safeQuerySelector(o,t=document){try{return t.querySelector(o)}catch(e){return console.warn(`[${l.name}] 选择器查询失败:`,o,e),null}},safeQuerySelectorAll(o,t=document){try{return Array.from(t.querySelectorAll(o))}catch(e){return console.warn(`[${l.name}] 选择器查询失败:`,o,e),[]}}},f=class f{static init(){this.initialized||(this.addSettingsButton(),this.initialized=!0)}static addSettingsButton(){const t=document.querySelector(l.selectors.searchLink),e=t?t.parentElement:null;if(e){const i=document.createElement("li"),n=document.createElement("a");n.href="javascript:;",n.textContent="脚本配置",i.appendChild(n),e.parentElement.insertBefore(i,e.nextSibling);const r=this.createSettingsPanel();n.addEventListener("click",a=>{a.preventDefault(),r.show()})}else this.addFallbackSettingsButton()}static addFallbackSettingsButton(){const t=document.getElementById("nav-s");if(t){const e=document.createElement("a");e.href="javascript:;",e.textContent="脚本配置",e.style.color="#fff",e.style.marginRight="15px",e.className="fr",t.insertBefore(e,t.firstChild);const i=this.createSettingsPanel();e.addEventListener("click",n=>{n.preventDefault(),i.show()})}}static createSettingsPanel(){document.body.insertAdjacentHTML("beforeend",`
      <div id="preview-settings-panel" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:white; padding:25px; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.2); z-index:10001;">
        <div id="settings-form">
          <h4 class="settings-subtitle">预览图数量</h4>
          <div class="settings-radio-group">
            <label class="radio-option">
              <input type="radio" name="MAX_PREVIEW_IMAGES" value="3" checked>
              <span class="radio-text">少量 (3张)</span>
            </label>
            <label class="radio-option">
              <input type="radio" name="MAX_PREVIEW_IMAGES" value="4">
              <span class="radio-text">适中 (4张)</span>
            </label>
            <label class="radio-option">
              <input type="radio" name="MAX_PREVIEW_IMAGES" value="5">
              <span class="radio-text">较多 (5张)</span>
            </label>
          </div>

          <h4 class="settings-subtitle">加载速度</h4>
          <div class="settings-radio-group">
            <label class="radio-option">
              <input type="radio" name="CONCURRENT_LIMIT" value="6">
              <span class="radio-text">稳定 (6个并发)</span>
            </label>
            <label class="radio-option">
              <input type="radio" name="CONCURRENT_LIMIT" value="9" checked>
              <span class="radio-text">平衡 (9个并发)</span>
            </label>
            <label class="radio-option">
              <input type="radio" name="CONCURRENT_LIMIT" value="12">
              <span class="radio-text">快速 (12个并发)</span>
            </label>
          </div>
        </div>
        <div class="settings-buttons">
          <button id="save-settings-btn">保存并刷新</button>
          <button id="close-settings-btn" style="margin-left:10px;">关闭</button>
        </div>
      </div>
      <div id="preview-settings-overlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:10000;"></div>
    `);const e=document.getElementById("preview-settings-panel"),i=document.getElementById("preview-settings-overlay"),n=document.getElementById("settings-form"),r=l.getAll();for(const s in r){const c=n.querySelector(`[name="${s}"][value="${r[s]}"]`);c&&(c.checked=!0)}document.getElementById("save-settings-btn").addEventListener("click",()=>{n.querySelectorAll('input[type="radio"]:checked').forEach(c=>{const p=parseInt(c.value,10);l.set(c.name,p)}),alert("设置已保存,页面将刷新以应用更改。"),window.location.reload()});const a=()=>{e.style.display="none",i.style.display="none"};return document.getElementById("close-settings-btn").addEventListener("click",a),i.addEventListener("click",a),{show:()=>{e.style.display="block",i.style.display="block"}}}};f.initialized=!1;let u=f;const d=class d{static init(){this.createLightbox(),this.setupEventListeners()}static createLightbox(){if(this.lightbox)return;this.lightbox=document.createElement("div"),this.lightbox.className="lightbox",this.lightboxContent=document.createElement("div"),this.lightboxContent.className="lightbox-content",this.lightboxImg=document.createElement("img"),this.lightboxImg.className="lightbox-image";const t=document.createElement("div");t.className="lightbox-close",t.innerHTML="×";const e=document.createElement("div");e.className="lightbox-prev",e.innerHTML="‹";const i=document.createElement("div");i.className="lightbox-next",i.innerHTML="›",this.loadingText=document.createElement("div"),this.loadingText.className="lightbox-loading",this.loadingText.textContent="加载中...",this.lightboxContent.appendChild(this.loadingText),this.lightboxContent.appendChild(this.lightboxImg),this.lightbox.appendChild(this.lightboxContent),this.lightbox.appendChild(t),this.lightbox.appendChild(e),this.lightbox.appendChild(i),document.body.appendChild(this.lightbox),this.closeBtn=t,this.prevBtn=e,this.nextBtn=i}static setupEventListeners(){!this.closeBtn||!this.prevBtn||!this.nextBtn||!this.lightbox||(this.closeBtn.addEventListener("click",()=>this.closeLightbox()),this.prevBtn.addEventListener("click",t=>{t.stopPropagation(),this.currentIndex=(this.currentIndex-1+this.currentImages.length)%this.currentImages.length,this.updateLightboxImage()}),this.nextBtn.addEventListener("click",t=>{t.stopPropagation(),this.currentIndex=(this.currentIndex+1)%this.currentImages.length,this.updateLightboxImage()}),this.lightbox.addEventListener("click",t=>{t.target===this.lightbox&&this.closeLightbox()}),document.addEventListener("keydown",t=>{this.lightbox?.classList.contains("active")&&(t.key==="Escape"?this.closeLightbox():t.key==="ArrowLeft"?this.prevBtn?.click():t.key==="ArrowRight"&&this.nextBtn?.click())}))}static updateLightboxImage(){if(!this.lightboxImg||!this.loadingText||!this.lightboxContent)return;const t=this.currentImages[this.currentIndex];this.loadingText.textContent="加载中...",this.loadingText.style.display="block",this.lightboxImg.style.display="none",this.lightboxImg.style.width="",this.lightboxImg.style.height="",this.lightboxContent.classList.remove("landscape","portrait"),this.lightboxImg.src=t,this.lightboxImg.onload=()=>{!this.lightboxImg||!this.loadingText||!this.lightboxContent||(this.loadingText.style.display="none",this.lightboxImg.style.display="block",this.lightboxImg.naturalWidth<300&&this.lightboxImg.naturalHeight<300?this.lightboxImg.style.width=this.lightboxImg.naturalWidth*2+"px":this.lightboxImg.naturalWidth>this.lightboxImg.naturalHeight?this.lightboxContent.classList.add("landscape"):this.lightboxContent.classList.add("portrait"))},this.lightboxImg.onerror=()=>{this.loadingText&&(this.loadingText.textContent="图片加载失败")}}static showLightbox(t,e){this.lightbox&&(this.currentImages=t,this.currentIndex=e,this.updateLightboxImage(),this.lightbox.classList.add("active"))}static closeLightbox(){this.lightbox&&this.lightbox.classList.remove("active")}};d.lightbox=null,d.lightboxImg=null,d.lightboxContent=null,d.loadingText=null,d.currentImages=[],d.currentIndex=0,d.closeBtn=null,d.prevBtn=null,d.nextBtn=null;let g=d;const v=class v{static injectStyles(){if(this.styleInjected||document.getElementById("preview-styles"))return;const t=document.createElement("style");t.id="preview-styles",t.textContent=`
      /* 标题行样式 - 直接使用悬浮后的蓝色效果 */
      .thread-title-highlighted {
        background-color: #e3f2fd !important;
        position: relative;
        border-top-left-radius: 6px;
        border-top-right-radius: 6px;
      }

      /* 点击复制后的冒泡提示 */
      .click-tip {
        position: fixed;
        background: #333;
        color: white;
        padding: 5px 10px;
        border-radius: 4px;
        font-size: 12px;
        z-index: 10000;
        pointer-events: none;
        transform: translate(15px, -15px);
        transition: opacity 0.2s;
        opacity: 0;
      }

      /* 预览容器样式 */
      .preview-container {
        margin: 0 0 10px 0;
        border: 1px solid #c8e1ff;
        border-top: none;
        border-bottom-left-radius: 6px;
        border-bottom-right-radius: 6px;
        padding: 15px;
        background-color: #f5faff;
        box-shadow: 0 2px 4px rgba(0,0,0,0.05);
      }
        
      .preview-section {
        margin-bottom: 12px;
        animation: fadeIn 0.4s ease-in-out;
      }
        
      .preview-section-title {
        font-size: 13px;
        color: #666;
        margin-bottom: 5px;
        font-weight: bold;
        padding-left: 5px;
        border-left: 3px solid #3498db;
      }
        
      @keyframes fadeIn {
        from { opacity: 0; transform: translateY(-10px); }
        to { opacity: 1; transform: translateY(0); }
      }
        
      .preview-images {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
        gap: 10px;
        margin-bottom: 15px;
      }
        
      .preview-image {
        width: 100%;
        height: 220px;
        object-fit: contain;
        background: transparent;
        border: none;
        border-radius: 0;
        transition: transform 0.2s;
        cursor: pointer;
      }
        
      .preview-image:hover {
        transform: scale(1.03);
      }
        
      .preview-filesize {
        color: #e74c3c;
        font-weight: bold;
        font-size: 14px;
      }
        
      .preview-magnet, .preview-bt-link {
        font-size: 14px;
        word-break: break-all;
        cursor: pointer;
        padding: 8px;
        background-color: #e8f4ff;
        border-radius: 4px;
        margin-bottom: 8px;
        box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        position: relative;
      }

      /* 大图查看样式 */
      .lightbox {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.85);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 9999;
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.3s ease, visibility 0.3s ease;
      }
        
      .lightbox.active {
        opacity: 1;
        visibility: visible;
      }
        
      .lightbox-content {
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
        max-width: 95%;
        max-height: 95%;
        transition: max-width 0.3s ease, max-height 0.3s ease;
      }
        
      .lightbox-content.landscape {
        max-width: 80vw; 
        max-height: 90vh;
      }
        
      .lightbox-content.portrait {
        max-width: 60vw;
        max-height: 90vh;
      }
        
      .lightbox-image {
        display: block;
        max-width: 100%;
        max-height: 100%;
        width: auto;
        height: auto;
        object-fit: contain;
        box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
      }
        
      .lightbox-prev, .lightbox-next {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        font-size: 50px;
        color: #fff;
        cursor: pointer;
        z-index: 10000;
        padding: 0 20px;
        user-select: none;
        transition: color .2s ease;
      }
        
      .lightbox-prev:hover, .lightbox-next:hover {
        color: #ccc;
      }
        
      .lightbox-prev { left: 15px; }
      .lightbox-next { right: 15px; }
        
      .lightbox-close {
        position: absolute;
        top: 20px;
        right: 20px;
        color: #fff;
        font-size: 30px;
        cursor: pointer;
      }
        
      .lightbox-loading {
        color: white;
        font-size: 16px;
      }

      /* 设置面板样式 */
      #preview-settings-panel {
        width: 480px;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      }
        
      #preview-settings-panel h3 {
        margin: 0 0 20px 0;
        padding-bottom: 15px;
        border-bottom: 1px solid #e0e0e0;
        font-size: 18px;
        color: #333;
      }
        
      .settings-subtitle {
        font-size: 16px;
        color: #444;
        margin-top: 25px;
        margin-bottom: 15px;
        padding-bottom: 8px;
      }
        
      .settings-subtitle:first-of-type {
        margin-top: 0;
      }
        
      .settings-radio-group {
        display: flex;
        flex-direction: column;
        gap: 12px;
        margin-bottom: 25px;
      }
        
      .radio-option {
        display: flex;
        align-items: center;
        padding: 10px 15px;
        border: 2px solid #e0e0e0;
        border-radius: 6px;
        cursor: pointer;
        transition: all 0.2s ease;
        background: #fafafa;
      }
        
      .radio-option:hover {
        border-color: #3498db;
        background: #f0f8ff;
      }
        
      .radio-option input[type="radio"] {
        margin-right: 10px;
        transform: scale(1.2);
      }
        
      .radio-option input[type="radio"]:checked + .radio-text {
        color: #3498db;
        font-weight: bold;
      }
        
      .radio-option:has(input[type="radio"]:checked) {
        border-color: #3498db;
        background: #e3f2fd;
      }
        
      .radio-text {
        font-size: 14px;
        color: #555;
        transition: color 0.2s ease;
      }
        
      .settings-buttons {
        margin-top: 20px;
        padding-top: 20px;
        border-top: 1px solid #e0e0e0;
        text-align: right;
      }
        
      .settings-buttons button {
        padding: 8px 16px;
        font-size: 14px;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        transition: opacity 0.2s;
      }
        
      .settings-buttons button:hover {
        opacity: 0.85;
      }
        
      #save-settings-btn {
        background-color: #28a745;
        color: white;
      }
        
      #close-settings-btn {
        background-color: #6c757d;
        color: white;
      }
    `,document.head.appendChild(t),this.styleInjected=!0}};v.styleInjected=!1;let x=v;class y{static removeAds(){document.querySelectorAll(l.selectors.threadRows).forEach(e=>{const i=e.querySelector("td.tal");i&&i.innerHTML.includes("headtopic_3.gif")&&e.remove()})}}class b{static extractImages(t){const e=l.get("MAX_PREVIEW_IMAGES");let i=[];for(const s of l.selectors.imgSelectors)if(i=Array.from(t.querySelectorAll(s)),i.length>0)break;let r=i.filter(s=>{const c=s.getAttribute("style")||"";return!(c.includes("display: none")||c.includes("display:none"))}).map(s=>({src:s.getAttribute("data-original")||s.getAttribute("src")||"",img:s})).filter(s=>{if(!s.src||!s.src.startsWith("http")||s.src.includes("loading.")||s.src.includes("placeholder."))return!1;const p=s.src.toLowerCase().split("/").pop()||"";return!(l.filters.badImagePatterns.some(m=>m.test(p))||s.img.width&&s.img.height&&(s.img.width<l.filters.minImageSize.width||s.img.height<l.filters.minImageSize.height))});return r.sort((s,c)=>{const p=/\.(jpg|jpeg|png)$/i.test(s.src),m=/\.(jpg|jpeg|png)$/i.test(c.src);return p&&!m?-1:!p&&m?1:0}),r.map(s=>s.src).slice(0,e)}static extractFileSize(t){let e="";for(const i of l.selectors.contentSelectors){const n=t.querySelector(i);if(n){let a=n.innerHTML.match(l.regex.fileSize);if(a&&(a[1]||a[2])){e=(a[1]||a[2]).trim();break}}}return e}static extractMagnet(t){let e="",i=t.querySelector(l.selectors.magnetTextarea);if(i)e=i.value.trim();else{let n=t.querySelector(l.selectors.magnetLink);if(n)e=n.getAttribute("href")||"";else{const a=t.body.innerHTML.match(l.regex.magnetHash);a&&a[1]&&(e=`magnet:?xt=urn:btih:${a[1]}`)}}return e}}class w{static buildPreviewUI(t,e){const{imgSrcs:i,fileSize:n,magnet:r}=e;if(t.nextElementSibling&&t.nextElementSibling.classList.contains("imagePreviewTr"))return;t.classList.add("thread-title-highlighted");const a=document.createElement("tr");a.className="imagePreviewTr";const s=document.createElement("td");s.colSpan=t.children.length;const c=document.createElement("div");c.className="preview-container",c.style.borderTop="none",i.length&&c.appendChild(this.createImageSection(i)),(n||r)&&c.appendChild(this.createInfoSection(n,r)),s.appendChild(c),a.appendChild(s),t.parentNode.insertBefore(a,t.nextSibling)}static createImageSection(t){const e=document.createElement("div");e.className="preview-section";const i=document.createElement("div");i.className="preview-section-title",i.textContent="预览图片",e.appendChild(i);const n=document.createElement("div");return n.className="preview-images",t.forEach((r,a)=>{if(r&&r.startsWith("http")){const s=document.createElement("img");s.src=r,s.className="preview-image",s.onerror=()=>{s.style.display="none"},s.addEventListener("click",()=>g.showLightbox(t,a)),n.appendChild(s)}}),e.appendChild(n),e}static createInfoSection(t,e){const i=document.createElement("div");i.className="preview-section";const n=document.createElement("div");if(n.className="preview-section-title",n.textContent="资源信息",i.appendChild(n),t){const r=document.createElement("div");r.className="preview-filesize";const a=t.includes("MB")||t.includes("mb")?"【影片大小】":"【影片容量】";r.innerHTML=`${a}:${t}`,r.style.marginBottom="10px",i.appendChild(r)}if(e){const r=document.createElement("div");r.className="preview-magnet",r.textContent=e,r.title="点击链接可复制",r.onclick=function(a){h.copyToClipboard(e,a)},i.appendChild(r)}return i}}class I{static async processThreadLink(t){const e=t.href;if(!e||!l.regex.threadUrl.test(e))return;const i=t.closest("tr");if(!(!i||i.querySelector('img[src*="headtopic"]')))try{const r=await(await fetch(e)).text(),s=new DOMParser().parseFromString(r,"text/html"),c={imgSrcs:b.extractImages(s),fileSize:b.extractFileSize(s),magnet:b.extractMagnet(s)};if(!c.imgSrcs.length&&!c.fileSize&&!c.magnet)return;w.buildPreviewUI(i,c)}catch(n){console.error(`[${l.name}] 预览加载失败:`,n)}}}class E{static async displayThreadImages(){const t=l.get("CONCURRENT_LIMIT");if(h.isContentPage())return;x.injectStyles(),h.removeRules(),g.init();const e=h.safeQuerySelectorAll(l.selectors.threadLinks);e.length&&await h.asyncPool(t,e,I.processThreadLink)}static async main(){try{u.init(),h.isContentPage()||(y.removeAds(),await this.displayThreadImages())}catch(t){console.error(`[${l.name}] 初始化失败:`,t)}}}window.addEventListener("load",()=>E.main())})();