您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
点击网页内链接,在弹窗中加载内容
// ==UserScript== // @name 页内弹窗打开链接 // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description 点击网页内链接,在弹窗中加载内容 // @author AI // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @connect * // @license MIT // ==/UserScript== (function() { 'use strict'; // 添加全局样式 (unchanged) GM_addStyle(` /* 基础链接样式优化 */ .suh a, table tr td a, th.common a { cursor: pointer; transition: color 0.2s ease-in-out, text-shadow 0.2s ease-in-out; text-decoration: none; position: relative; color: inherit; } .suh a:hover, table tr td a:hover, th.common a.xst:hover { color: #2979ff; text-shadow: 0 0 5px rgba(41, 121, 255, 0.3); } /* 面板核心样式 (弹窗模式) */ #popup-content-panel { position: fixed; z-index: 10000; background-color: #fff; box-shadow: 0 5px 35px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: hidden; opacity: 0; pointer-events: none; width: 84vw; height: 96vh; max-width: 1080px; max-height: 840px; top: 50%; left: 50%; border: 1px solid #ccc; border-radius: 8px; transform: translate(-50%, -50%) scale(0.95); transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.16, 1, 0.3, 1); } #popup-content-panel.visible { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); } /* 面板头部 */ #popup-panel-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; background-color: #f9f9f9; border-bottom: 1px solid #e0e0e0; height: 52px; min-height: 52px; box-sizing: border-box; cursor: grab; } #popup-panel-header:active { cursor: grabbing; } #popup-panel-title { font-size: 16px; font-weight: 500; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 130px); user-select: none; } #popup-panel-actions { display: flex; gap: 5px; } .popup-panel-btn { background-color: transparent; border: 1px solid transparent; border-radius: 4px; padding: 5px 7px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, transform 0.1s ease-in-out; color: #555; } .popup-panel-btn svg { width: 16px; height: 16px; } .popup-panel-btn:hover { background-color: #e9e9e9; color: #333; } .popup-panel-btn:active { transform: translateY(0.5px); background-color: #dcdcdc; } .popup-panel-btn svg { pointer-events: none; } #popup-panel-close:hover { color: #d32f2f; } #popup-panel-refresh:hover, #popup-panel-open-in-new:hover, #popup-panel-maximize:hover { color: #1976d2; } /* 内容区域 */ #popup-content-area { flex: 1; overflow-y: auto; position: relative; background-color: #fff; padding: 15px; box-sizing: border-box; scroll-behavior: smooth; } #popup-content-area.iframe-direct-load { padding: 0; /* Remove padding when iframe loads src directly */ } /* 加载与错误状态 */ #popup-panel-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #777; padding: 25px; } .spinner { width: 36px; height: 36px; margin-bottom: 18px; border: 3px solid rgba(41, 121, 255, 0.2); border-radius: 50%; border-top: 3px solid #2979ff; animation: spin 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #popup-panel-error { padding: 35px; color: #c62828; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; box-sizing: border-box; } #popup-panel-error h3 { margin-top: 18px; margin-bottom: 12px; font-weight: 500; font-size: 1.2em; } #popup-panel-error p { margin-bottom: 22px; color: #777; max-width: 420px; line-height: 1.6; } #popup-panel-error button { padding: 11px 22px; background: #2979ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: background 0.15s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.15s ease-in-out; } #popup-panel-error button:hover { background: #4d90fe; transform: translateY(-0.5px); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } /* Iframe */ #popup-panel-iframe { width: 100%; height: 100%; border: none; background-color: white; } /* 链接视觉提示 (unchanged from v1.7) */ .popup-trigger::after, th.common::after, a.xst::after { content: ''; /* Material Symbols Outlined: open_in_new_down */ font-family: 'Material Symbols Outlined'; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 16px; opacity: 0; transition: opacity 0.2s ease-in-out, transform 0.15s ease-in-out; pointer-events: none; } div.items-content-tittle.popup-trigger { position: relative; } div.items-content-tittle.popup-trigger::after { right: 5px; } div.tittle_data.popup-trigger { position: relative; padding-right: 25px; } div.tittle_data.popup-trigger::after { right: 5px; } div.items-content-remark.popup-trigger { position: relative; padding-right: 25px; } div.items-content-remark.popup-trigger::after { right: 5px; } div.items-content-tittle.popup-trigger { padding-right: 25px; } .popup-trigger:hover::after, th.common:hover::after, a.xst:hover::after, div.items-content-tittle.popup-trigger:hover::after, div.tittle_data.popup-trigger:hover::after, div.items-content-remark.popup-trigger:hover::after { opacity: 0.7; transform: translateY(-50%) scale(1.05); } th.common { position: relative; } a.xst { position: relative; display: inline-block; transition: color 0.15s ease-in-out, padding-right 0.15s ease-in-out; padding-right: 20px; } a.xst:hover { color: #2979ff; padding-right: 25px; } a.xst::after { right: 0; } /* 遮罩层 (unchanged) */ #popup-panel-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.2); opacity: 0; z-index: 9998; transition: opacity 0.3s ease-in-out; pointer-events: none; backdrop-filter: blur(3px); } #popup-panel-overlay.visible { opacity: 1; pointer-events: auto; } /* 悬浮开关按钮容器与关闭按钮 */ #popup-toggle-container { position: fixed; right: 14px; bottom: 16px; z-index: 10003; display: flex; align-items: center; gap: 6px; } #popup-toggle-btn { background: #2979ff; color: #fff; border: none; border-radius: 20px; padding: 8px 12px; font-size: 12px; line-height: 1; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.18); display: flex; align-items: center; gap: 6px; transition: opacity .2s ease, transform .1s ease, background .2s ease; opacity: .85; } #popup-toggle-btn:hover { opacity: 1; transform: translateY(-1px); } #popup-toggle-btn.disabled { background: #9e9e9e; } #popup-toggle-btn svg { width: 14px; height: 14px; } #popup-toggle-hide { width: 22px; height: 22px; border-radius: 50%; border: none; background: rgba(0,0,0,0.35); color: #fff; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transform: translateY(1px); transition: opacity .18s ease, transform .12s ease, background .15s ease; pointer-events: none; } #popup-toggle-hide:hover { background: rgba(0,0,0,0.5); } #popup-toggle-container:hover #popup-toggle-hide { opacity: 1; transform: translateY(0); pointer-events: auto; } `); // SVG 图标定义 (unchanged) const ICONS = { close: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>', external: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>', refresh: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"></path><path d="M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>', maximize: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg>', minimize: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg>' }; // 全局变量 (unchanged) let currentUrl = ""; let isFullScreen = false; let panelPreFullScreenDimensions = {}; let maximizeBtnElement; let clickHandlerAttached = false; let mutationObserver = null; // 禁用站点存储(按主域名 eTLD+1 粒度) function computeRegistrableDomain(hostname) { try { if (!hostname) return ''; // IP 和 localhost 直接返回 if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname === 'localhost') return hostname; const parts = hostname.split('.'); if (parts.length <= 2) return hostname; const twoLabel = parts.slice(-2).join('.'); const threeLabel = parts.slice(-3).join('.'); const twoPartSuffixes = new Set([ 'co.uk','ac.uk','gov.uk','org.uk','ltd.uk','plc.uk','me.uk', 'com.cn','net.cn','org.cn','gov.cn','edu.cn','ac.cn', 'com.au','net.au','org.au','edu.au','gov.au', 'co.jp','ne.jp','or.jp','go.jp','ac.jp', 'co.kr','ne.kr','or.kr','go.kr','ac.kr' ]); if (twoPartSuffixes.has(twoLabel) && parts.length >= 3) { return threeLabel; } return twoLabel; } catch (_) { return hostname || ''; } } function loadDisabledHosts() { try { if (typeof GM_getValue === 'function') { const list = GM_getValue('popup_disabled_hosts', []); const set = new Set(); if (Array.isArray(list)) { list.forEach(item => { const host = String(item || '').split(':')[0]; const domain = computeRegistrableDomain(host); if (domain) set.add(domain); }); } // 迁移:保存为标准化主域 try { if (typeof GM_setValue === 'function') GM_setValue('popup_disabled_hosts', Array.from(set)); } catch (_) {} return set; } } catch (_) {} try { const raw = localStorage.getItem('popup_disabled_hosts'); const list = raw ? JSON.parse(raw) : []; const set = new Set(); if (Array.isArray(list)) { list.forEach(item => { const host = String(item || '').split(':')[0]; const domain = computeRegistrableDomain(host); if (domain) set.add(domain); }); } // 迁移保存 try { localStorage.setItem('popup_disabled_hosts', JSON.stringify(Array.from(set))); } catch (_) {} return set; } catch (_) { return new Set(); } } function saveDisabledHosts(set) { const arr = Array.from(set); try { if (typeof GM_setValue === 'function') GM_setValue('popup_disabled_hosts', arr); } catch (_) {} try { localStorage.setItem('popup_disabled_hosts', JSON.stringify(arr)); } catch (_) {} } const disabledHosts = loadDisabledHosts(); function getCurrentHostKey() { return computeRegistrableDomain(window.location.hostname); } function isCurrentHostDisabled() { return disabledHosts.has(getCurrentHostKey()); } function setCurrentHostDisabled(disabled) { const key = getCurrentHostKey(); if (disabled) disabledHosts.add(key); else disabledHosts.delete(key); saveDisabledHosts(disabledHosts); } function ensureToggleButton() { let container = document.getElementById('popup-toggle-container'); let btn = document.getElementById('popup-toggle-btn'); let hideBtn = document.getElementById('popup-toggle-hide'); if (!container) { container = document.createElement('div'); container.id = 'popup-toggle-container'; document.body.appendChild(container); } if (!btn) { btn = document.createElement('button'); btn.id = 'popup-toggle-btn'; btn.type = 'button'; btn.title = '切换本网站弹窗处理开关'; container.appendChild(btn); btn.addEventListener('click', () => { const toDisable = !isCurrentHostDisabled(); setCurrentHostDisabled(toDisable); updateToggleButtonState(); if (toDisable) { disableHandlers(); closePanel(); } else { enableHandlers(); } }); } if (!hideBtn) { hideBtn = document.createElement('button'); hideBtn.id = 'popup-toggle-hide'; hideBtn.type = 'button'; hideBtn.title = '隐藏此按钮(刷新后恢复)'; hideBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'; container.appendChild(hideBtn); hideBtn.addEventListener('click', (e) => { e.stopPropagation(); container.style.display = 'none'; }); } updateToggleButtonState(); } function updateToggleButtonState() { const btn = document.getElementById('popup-toggle-btn'); if (!btn) return; const disabled = isCurrentHostDisabled(); btn.classList.toggle('disabled', disabled); btn.innerHTML = disabled ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg><span>已禁用</span>' : '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9 12l2 2 4-4"/></svg><span>已启用</span>'; } function enableHandlers() { if (!clickHandlerAttached) { document.addEventListener("click", handleLinkClick, true); clickHandlerAttached = true; } const runEnhancements = () => { enhanceCiliLinks(); enhanceForumLinks(); enhanceTgbShuoLinks(); enhanceTgbBlogLinks(); enhanceTgbGenericItemLinks(); }; runEnhancements(); if (!mutationObserver) { mutationObserver = new MutationObserver(runEnhancements); mutationObserver.observe(document.body, { childList: true, subtree: true }); } } function disableHandlers() { if (clickHandlerAttached) { document.removeEventListener("click", handleLinkClick, true); clickHandlerAttached = false; } if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; } } // --- Panel Creation and Management Functions (unchanged) --- function createOverlay() { if (document.getElementById("popup-panel-overlay")) return document.getElementById("popup-panel-overlay"); const overlay = document.createElement("div"); overlay.id = "popup-panel-overlay"; overlay.addEventListener("click", closePanel); document.body.appendChild(overlay); return overlay; } function createContentPanel() { if (document.getElementById("popup-content-panel")) return document.getElementById("popup-content-panel"); createOverlay(); const popupPanel = document.createElement("div"); popupPanel.id = "popup-content-panel"; const header = document.createElement("div"); header.id = "popup-panel-header"; const title = document.createElement("div"); title.id = "popup-panel-title"; const actions = document.createElement("div"); actions.id = "popup-panel-actions"; const refreshBtn = document.createElement("button"); refreshBtn.id = "popup-panel-refresh"; refreshBtn.className = "popup-panel-btn"; refreshBtn.innerHTML = ICONS.refresh; refreshBtn.title = "刷新内容 (R)"; refreshBtn.onclick = () => { if (currentUrl) loadContent(currentUrl); }; maximizeBtnElement = document.createElement("button"); maximizeBtnElement.id = "popup-panel-maximize"; maximizeBtnElement.className = "popup-panel-btn"; maximizeBtnElement.onclick = toggleFullScreen; const openBtn = document.createElement("button"); openBtn.id = "popup-panel-open-in-new"; openBtn.className = "popup-panel-btn"; openBtn.innerHTML = ICONS.external; openBtn.title = "在新标签页打开"; openBtn.onclick = () => { if (currentUrl) window.open(currentUrl, "_blank"); }; const closeBtn = document.createElement("button"); closeBtn.id = "popup-panel-close"; closeBtn.className = "popup-panel-btn"; closeBtn.innerHTML = ICONS.close; closeBtn.title = "关闭 (Esc)"; closeBtn.onclick = closePanel; const contentArea = document.createElement("div"); contentArea.id = "popup-content-area"; actions.appendChild(refreshBtn); actions.appendChild(maximizeBtnElement); actions.appendChild(openBtn); actions.appendChild(closeBtn); header.appendChild(title); header.appendChild(actions); popupPanel.appendChild(header); popupPanel.appendChild(contentArea); document.body.appendChild(popupPanel); document.addEventListener("keydown", (e) => { const panel = document.getElementById("popup-content-panel"); if (panel && panel.classList.contains("visible")) { if (e.key === "Escape") closePanel(); else if (e.key === "f" || e.key === "F") toggleFullScreen(); else if (e.key === "r" || e.key === "R") { if (currentUrl && !e.ctrlKey && !e.metaKey) { e.preventDefault(); loadContent(currentUrl); } } } }); header.addEventListener("dblclick", (e) => { if (e.target.closest('button')) return; toggleFullScreen(); }); return popupPanel; } function updateMaximizeButtonIcon() { if (maximizeBtnElement) { maximizeBtnElement.innerHTML = isFullScreen ? ICONS.minimize : ICONS.maximize; maximizeBtnElement.title = isFullScreen ? "恢复 (F)" : "全屏 (F)"; } } function toggleFullScreen() { const popupPanel = document.getElementById("popup-content-panel"); if (!popupPanel) return; if (!isFullScreen) { panelPreFullScreenDimensions = { width: popupPanel.style.width, height: popupPanel.style.height, top: popupPanel.style.top, left: popupPanel.style.left, transform: popupPanel.style.transform, borderRadius: popupPanel.style.borderRadius, maxWidth: popupPanel.style.maxWidth, maxHeight: popupPanel.style.maxHeight }; popupPanel.style.transition = 'none'; Object.assign(popupPanel.style, { width: "100vw", height: "100vh", top: "0px", left: "0px", transform: "none", borderRadius: "0px", maxWidth: "none", maxHeight: "none", zIndex: "10002" }); void popupPanel.offsetWidth; popupPanel.style.transition = ''; isFullScreen = true; } else { popupPanel.style.transition = 'none'; Object.assign(popupPanel.style, { width: panelPreFullScreenDimensions.width || '', height: panelPreFullScreenDimensions.height || '', top: panelPreFullScreenDimensions.top || '', left: panelPreFullScreenDimensions.left || '', transform: panelPreFullScreenDimensions.transform || '', borderRadius: panelPreFullScreenDimensions.borderRadius || '', maxWidth: panelPreFullScreenDimensions.maxWidth || '', maxHeight: panelPreFullScreenDimensions.maxHeight || '', zIndex: "10000" }); void popupPanel.offsetWidth; popupPanel.style.transition = ''; isFullScreen = false; } updateMaximizeButtonIcon(); } function closePanel() { const popupPanel = document.getElementById("popup-content-panel"); const overlay = document.getElementById("popup-panel-overlay"); if (!popupPanel) return; if (isFullScreen) toggleFullScreen(); popupPanel.classList.remove("visible"); if (overlay) overlay.classList.remove("visible"); setTimeout(() => { const contentArea = document.getElementById("popup-content-area"); if (contentArea) contentArea.innerHTML = ""; currentUrl = ""; }, 350); } function showPanel(titleText, urlToLoad) { const popupPanel = createContentPanel(); const overlay = document.getElementById("popup-panel-overlay"); currentUrl = urlToLoad; document.getElementById("popup-panel-title").textContent = titleText || "查看内容"; popupPanel.classList.remove('visible'); Object.assign(popupPanel.style, { width: '', height: '', top: '', left: '', transform: '', maxWidth: '', maxHeight: '', borderRadius: '', zIndex: '' }); isFullScreen = false; updateMaximizeButtonIcon(); requestAnimationFrame(() => { popupPanel.classList.add("visible"); if (overlay) overlay.classList.add("visible"); }); return document.getElementById("popup-content-area"); } function showLoading(container) { container.classList.remove('iframe-direct-load'); container.innerHTML = ` <div id="popup-panel-loading"> <div class="spinner"></div> <div style="font-size: 15px; font-weight: 500;">正在加载内容...</div> <div style="margin-top: 10px; font-size: 13px; color: #999;">请稍候片刻</div> </div>`; } function showError(container, message) { container.classList.remove('iframe-direct-load'); const escapedUrl = currentUrl ? currentUrl.replace(/'/g, "\\'") : ''; container.innerHTML = ` <div id="popup-panel-error"> <div><svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="#c62828" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg></div> <h3 style="margin-top:15px;margin-bottom:10px;">加载失败</h3> <p>${message || "无法加载请求的内容"}</p> <button onclick="var urlToOpen = '${escapedUrl}'; if(urlToOpen) window.open(urlToOpen, '_blank');" style="margin-top:15px;padding:8px 16px;background:#d32f2f;color:white;border:none;border-radius:4px;cursor:pointer;" ${!escapedUrl ? 'disabled' : ''}> 在新标签页打开 </button> </div>`; } function loadContent(url) { const contentArea = document.getElementById("popup-content-area"); if (!contentArea) return; contentArea.classList.remove('iframe-direct-load'); // Reset class contentArea.innerHTML = ""; // Clear previous content before showing loading or iframe if (url.includes("linux.do")) { console.log(`[PopupViewer] Attempting direct iframe load for: ${url}`); showLoading(contentArea); // Show spinner HTML const iframe = document.createElement("iframe"); iframe.id = "popup-panel-iframe"; iframe.sandbox = "allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts"; // Note: allow-top-navigation and allow-popups-to-escape-sandbox are omitted to prevent breaking out let loadTimeoutCleared = false; const loadTimeout = setTimeout(() => { if (loadTimeoutCleared) return; loadTimeoutCleared = true; console.warn(`[PopupViewer] Timeout waiting for ${url} to load directly.`); if (document.getElementById('popup-panel-loading')) { showError(contentArea, `加载 ${url} 超时或被阻止。请尝试在新标签页打开。`); } }, 15000); // 15 seconds timeout iframe.onload = () => { if (loadTimeoutCleared) return; // Avoid acting if timeout already processed this loadTimeoutCleared = true; clearTimeout(loadTimeout); console.log(`[PopupViewer] iframe onload event for ${url}.`); // The iframe has loaded. Remove spinner and ensure iframe is styled. // The spinner was the content of contentArea. Now replace it. const loadingSpinner = document.getElementById('popup-panel-loading'); if (loadingSpinner && loadingSpinner.parentNode === contentArea) { contentArea.removeChild(loadingSpinner); } // If the iframe isn't already in contentArea (e.g., if an error cleared it, though unlikely here), // or if something else replaced the spinner, ensure the iframe is the sole content. if (!contentArea.contains(iframe)) { contentArea.innerHTML = ''; // Clear anything else contentArea.appendChild(iframe); } contentArea.classList.add('iframe-direct-load'); }; iframe.onerror = () => { if (loadTimeoutCleared) return; loadTimeoutCleared = true; clearTimeout(loadTimeout); console.error(`[PopupViewer] Error loading ${url} directly into iframe via onerror.`); showError(contentArea, `加载 ${url} 失败。`); }; // Replace the loading spinner content with the iframe element itself. // Setting iframe.src will trigger the load. contentArea.innerHTML = ''; // Clear spinner HTML contentArea.appendChild(iframe); iframe.src = url; // Set src AFTER appending to ensure onload/onerror are attached return; } // Default method for other sites (GM_xmlhttpRequest) showLoading(contentArea); GM_xmlhttpRequest({ method: "GET", url: url, onload: (response) => { if (response.status === 200) { try { contentArea.innerHTML = ""; // Clear loading message const iframe = document.createElement("iframe"); iframe.id = "popup-panel-iframe"; iframe.sandbox = "allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"; contentArea.appendChild(iframe); const iframeDoc = iframe.contentWindow.document; iframeDoc.open(); let baseHref = url; try { const urlObj = new URL(url); baseHref = urlObj.origin ? `${urlObj.origin}${urlObj.pathname}` : urlObj.pathname; } catch (e) { console.warn("Could not create base URL from:", url, e); } iframeDoc.write(`<!DOCTYPE html><html><head><base href="${baseHref}"><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Content</title></head><body>${response.responseText}</body></html>`); iframeDoc.close(); iframe.onload = () => { try { const links = iframeDoc.querySelectorAll("a[href]"); links.forEach(link => { link.target = "_blank"; try { if (!link.getAttribute('href')?.startsWith('javascript:')) { const absoluteUrl = new URL(link.getAttribute('href'), iframeDoc.baseURI).href; link.href = absoluteUrl; } } catch (e) { /* console.warn("Could not absolutize URL:", link.getAttribute('href'), e); */ } }); const style = iframeDoc.createElement('style'); style.textContent = `body { font-family: Segoe UI, sans-serif; padding: 10px; word-wrap: break-word; overflow-wrap: break-word; } img, video, iframe { max-width: 100%; height: auto; } a { color: #007bff; text-decoration: none; } a:hover { text-decoration: underline; } a:visited { color: #6a0dad; }`; iframeDoc.head.appendChild(style); } catch (iframeError) { console.error("Error manipulating iframe content:", iframeError); } }; if (iframe.contentWindow && iframe.contentWindow.document.readyState === 'complete') { iframe.onload(); } } catch (error) { showError(contentArea, "内容解析失败: " + error.message); console.error("Error processing content:", error); } } else { showError(contentArea, `加载失败 (HTTP ${response.status})`); } }, onerror: (error) => { showError(contentArea, "网络请求失败"); console.error("GM_xmlhttpRequest error:", error); }, }); } // --- Refactored Link Handling Logic (unchanged from v1.6) --- function isLinkSamePageAnchor(href) { try { const currentLoc = new URL(window.location.href); const linkLoc = new URL(href, window.location.href); return linkLoc.origin === currentLoc.origin && linkLoc.pathname === currentLoc.pathname && linkLoc.hash && linkLoc.href !== currentLoc.href; } catch (e) { return false; } } function isAssetLikeUrl(urlString) { try { const urlObj = new URL(urlString, window.location.href); const pathname = urlObj.pathname.toLowerCase(); const assetExtensions = [ '.jpg','.jpeg','.png','.gif','.webp','.svg','.avif','.ico', '.mp3','.wav','.ogg','.flac','.mp4','.webm','.mkv','.mov', '.pdf','.zip','.rar','.7z','.tar','.gz','.bz2','.xz','.dmg','.exe','.apk', '.woff','.woff2','.ttf','.otf','.eot','.wasm' ]; return assetExtensions.some(ext => pathname.endsWith(ext)); } catch (_) { return false; } } function shouldHandleAsInnerPage(href) { if (!href) return false; if (href.startsWith('javascript:')) return false; if (href.startsWith('mailto:') || href.startsWith('tel:')) return false; if (isAssetLikeUrl(href)) return false; try { const current = new URL(window.location.href); const link = new URL(href, window.location.href); // 仅处理同源或同主域的普通页面链接 return link.origin === current.origin && !isLinkSamePageAnchor(link.href); } catch (_) { return false; } } function activateLinkInPanel(event, clickedElement, title, urlToOpen) { event.preventDefault(); event.stopPropagation(); showPanel(title, urlToOpen); loadContent(urlToOpen); } function handleForumLink_XST(event) { const link = event.target.closest("a.xst"); if (link && link.href && !link.href.startsWith('javascript:')) { if (isLinkSamePageAnchor(link.href)) { return false; } const title = link.textContent.trim() || "查看帖子"; const resolvedUrl = new URL(link.href, window.location.origin).href; activateLinkInPanel(event, link, title, resolvedUrl); return true; } return false; } function handleTGBGenericItemClick(event) { const clickedElement = event.target; const titleDiv = clickedElement.closest('div.items-content-tittle.popup-trigger'); const remarkDiv = clickedElement.closest('div.items-content-remark.popup-trigger'); const targetContainer = titleDiv || remarkDiv; if (!targetContainer || !targetContainer.closest('div.items-list-content')) { return false; } const hostname = window.location.hostname; const pathname = window.location.pathname; if (!(hostname === "www.tgb.cn" && !pathname.startsWith("/blog/") && !pathname.startsWith("/user/blog/"))) { return false; } let linkElement = null; let urlToOpen = null; let titleText = ""; if (targetContainer.parentElement.tagName === 'A') { linkElement = targetContainer.parentElement; const href = linkElement.getAttribute('href'); if (href && !href.startsWith('javascript:')) { try { urlToOpen = new URL(href, window.location.origin).href; } catch (e) { console.warn("Error resolving href in TGB Generic (parent A):", href, e); } } titleText = linkElement.title || targetContainer.textContent.trim() || "淘股吧内容"; } else { linkElement = targetContainer.querySelector('a'); if (linkElement) { const dataHref = linkElement.dataset.href; const href = linkElement.getAttribute('href'); if (dataHref) { try { urlToOpen = new URL(dataHref, window.location.origin).href; } catch (e) { console.warn("Error resolving data-href in TGB Generic (child A):", dataHref, e); } } if (!urlToOpen && href && !href.startsWith('javascript:')) { try { urlToOpen = new URL(href, window.location.origin).href; } catch (e) { console.warn("Error resolving href in TGB Generic (child A fallback):", href, e); } } titleText = linkElement.title || linkElement.textContent.trim() || targetContainer.textContent.trim() || "淘股吧内容"; } } if (linkElement && urlToOpen && !isLinkSamePageAnchor(urlToOpen)) { activateLinkInPanel(event, linkElement, titleText, urlToOpen); return true; } return false; } const COMMON_SITE_HANDLERS = [ { name: "GitHub Issues", condition: (hostname, pathname) => hostname === "github.com" && pathname.includes("/issues"), linkSelector: 'a.IssuePullRequestTitle-module__ListItemTitle_1--_xOfg', parentSelector: 'div.IssueRow-module__row--XmR1f', titleExtractor: (el) => el.textContent.trim() || "查看 GitHub Issue", }, { name: "Linux.do Topics", condition: (hostname, pathname) => hostname === "linux.do", linkSelector: 'a.title.raw-link.raw-topic-link', parentSelector: 'tr.topic-list-item', titleExtractor: (el) => el.textContent.trim() || "查看主题", }, { name: "TGB Shuo Livenews", condition: (hostname, pathname) => hostname === "shuo.tgb.cn" && pathname.startsWith("/livenews/"), linkSelector: 'div.items-content-tittle a', parentSelector: 'div.items-list-content', titleExtractor: (el) => el.textContent.trim() || "查看资讯", }, { name: "TGB Blog", condition: (hostname, pathname) => hostname === "www.tgb.cn" && pathname.startsWith("/blog/"), linkSelector: 'div.tittle_data a', parentSelector: 'div.article_tittle', titleExtractor: (el) => el.title || el.textContent.trim() || "查看博客", } ]; function handleLinkClick(event) { if (event.target.closest("#popup-content-panel") && !event.target.closest('.popup-panel-btn')) { return; } const currentHostname = window.location.hostname; const currentPathname = window.location.pathname; if (handleForumLink_XST(event)) return; if (handleTGBGenericItemClick(event)) return; if (currentHostname.includes("cili.")) { const target = event.target; const tableRow = target.closest("tr"); if (tableRow) { const firstTd = tableRow.querySelector("td:first-child"); if (firstTd && firstTd.contains(target)) { const linkCell = firstTd.querySelector("a"); if (linkCell && linkCell.href && !linkCell.href.startsWith('javascript:')) { const resolvedUrl = new URL(linkCell.href, window.location.origin).href; if (isLinkSamePageAnchor(resolvedUrl)) { return; } let title = (linkCell.querySelector("b")?.textContent || linkCell.textContent).trim() || "查看内容"; activateLinkInPanel(event, linkCell, title, resolvedUrl); return; } } } } for (const handler of COMMON_SITE_HANDLERS) { if (handler.condition(currentHostname, currentPathname)) { const linkElement = event.target.closest(handler.linkSelector) || (event.target.matches(handler.linkSelector) ? event.target : null); if (linkElement && (!handler.parentSelector || linkElement.closest(handler.parentSelector))) { let resolvedUrl; const hrefAttr = linkElement.getAttribute('href'); if (hrefAttr && !hrefAttr.startsWith('javascript:')) { try { resolvedUrl = new URL(hrefAttr, window.location.origin).href; } catch(e) { console.warn("Error resolving href for common handler link:", hrefAttr, e); resolvedUrl = null; } } if (!resolvedUrl) { continue; } if (isLinkSamePageAnchor(resolvedUrl)) { return; } const title = handler.titleExtractor(linkElement); activateLinkInPanel(event, linkElement, title, resolvedUrl); return; } } } const targetSuhTd = event.target.closest("td.suh"); if (targetSuhTd) { const link = event.target.closest("a") || targetSuhTd.querySelector("a"); if (link && link.href && !link.href.startsWith('javascript:')) { const resolvedUrl = new URL(link.href, window.location.origin).href; if (isLinkSamePageAnchor(resolvedUrl)) { return; } const title = link.title || link.textContent.trim() || "查看内容"; activateLinkInPanel(event, link, title, resolvedUrl); return; } } // 通用兜底:对所有页面的同域内页链接启用弹窗 // 跳过带功能键、鼠标中键、具有显式 target 的新开标签 if (event.defaultPrevented) return; if (event.button !== 0) return; // only left click if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; const anyLink = event.target.closest('a[href]'); if (anyLink) { const hrefAttr = anyLink.getAttribute('href') || ''; const targetAttr = (anyLink.getAttribute('target') || '').toLowerCase(); if (targetAttr === '_blank') return; if (shouldHandleAsInnerPage(hrefAttr)) { let resolvedUrl; try { resolvedUrl = new URL(hrefAttr, window.location.href).href; } catch (_) {} if (resolvedUrl) { const title = anyLink.title || anyLink.textContent.trim() || '查看内容'; activateLinkInPanel(event, anyLink, title, resolvedUrl); return; } } } } // --- Link Enhancement Functions (unchanged from v1.6) --- function enhanceCiliLinks() { if (!window.location.hostname.includes("cili.")) return; document.querySelectorAll("tr").forEach(row => { const firstCell = row.querySelector("td:first-child"); const link = firstCell ? firstCell.querySelector("a") : null; if (firstCell && link && link.href && !link.href.startsWith('javascript:')) { if (isLinkSamePageAnchor(new URL(link.href, window.location.origin).href)) return; firstCell.classList.add("popup-trigger"); } }); } function enhanceForumLinks() { document.querySelectorAll("a.xst").forEach(link => { if (link.href && !link.href.startsWith('javascript:')) { if (isLinkSamePageAnchor(new URL(link.href, window.location.origin).href)) return; const th = link.closest("th.common, th.new, th.lock"); if (th && !th.classList.contains('common')) { th.classList.add("common"); } } }); } function enhanceTgbShuoLinks() { if (!(window.location.hostname === "shuo.tgb.cn" && window.location.pathname.startsWith("/livenews/"))) return; document.querySelectorAll('div.items-content-tittle').forEach(titleDiv => { const link = titleDiv.querySelector('a'); if (link && link.href && !link.href.startsWith('javascript:')) { if (isLinkSamePageAnchor(new URL(link.href, window.location.origin).href)) return; titleDiv.classList.add("popup-trigger"); } }); } function enhanceTgbBlogLinks() { if (!(window.location.hostname === "www.tgb.cn" && window.location.pathname.startsWith("/blog/"))) return; document.querySelectorAll('div.article_tittle').forEach(articleTitleDiv => { const titleDataDiv = articleTitleDiv.querySelector('div.tittle_data'); const link = titleDataDiv ? titleDataDiv.querySelector('a') : null; if (titleDataDiv && link && link.href && !link.href.startsWith('javascript:')) { if (isLinkSamePageAnchor(new URL(link.href, window.location.origin).href)) return; titleDataDiv.classList.add("popup-trigger"); } }); } function enhanceTgbGenericItemLinks() { const hostname = window.location.hostname; const pathname = window.location.pathname; if (!(hostname === "www.tgb.cn" && !pathname.startsWith("/blog/") && !pathname.startsWith("/user/blog/"))) { return; } document.querySelectorAll('div.items-content-tittle, div.items-content-remark').forEach(containerDiv => { if (!containerDiv.closest('div.items-list-content')) return; let linkElement = null; let resolvedUrl = null; const innerLink = containerDiv.querySelector('a'); if (innerLink) { const dataHref = innerLink.dataset.href; const href = innerLink.getAttribute('href'); if (dataHref) { try { resolvedUrl = new URL(dataHref, window.location.origin).href; linkElement = innerLink; } catch (e) {} } if (!resolvedUrl && href && !href.startsWith('javascript:')) { try { resolvedUrl = new URL(href, window.location.origin).href; linkElement = innerLink; } catch (e) {} } } if (!linkElement && containerDiv.parentElement.tagName === 'A') { const parentLink = containerDiv.parentElement; const href = parentLink.getAttribute('href'); if (href && !href.startsWith('javascript:')) { try { resolvedUrl = new URL(href, window.location.origin).href; linkElement = parentLink; } catch (e) {} } } if (linkElement && resolvedUrl && !isLinkSamePageAnchor(resolvedUrl)) { containerDiv.classList.add("popup-trigger"); } }); } // --- Initialization --- function init() { const materialFontLink = document.createElement('link'); materialFontLink.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200'; materialFontLink.rel = 'stylesheet'; document.head.appendChild(materialFontLink); ensureToggleButton(); if (isCurrentHostDisabled()) { disableHandlers(); } else { enableHandlers(); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();