// ==UserScript== // @name A岛引用查看增强 // @namespace http://tampermonkey.net/ // @version 0.1.1 // @description 让A到网页端的引用支持嵌套查看、固定、折叠等功能 // @author FToovvr // @license MIT; https://opensource.org/licenses/MIT // @include /^https?://(adnmb\d*.com|tnmb.org)/.*$/ // @grant none // @downloadURL none // ==/UserScript== // TODO: 把一看到的纳入缓存 // TODO: 持久化缓存 // TODO: 刷新按钮 // TODO: 自定义配置页 https://stackoverflow.com/a/43462416 // TODO: 20秒超时/异常处理 // TODO: 更好的「加载中…」? // TODO: 悬浮淡入、淡出 (function () { 'use strict'; function entry() { const model = new Model(); if (!model.isSupported) { console.log("浏览器功能不支持「A岛引用增强」脚本。"); return; } // 销掉原先的预览方法 document.querySelectorAll('font[color="#789922"]').forEach((elem) => { const newElem = elem.cloneNode(true); elem.parentNode.replaceChild(newElem, elem); }); ViewHelper.setupStyle(); ViewHelper.setupContent(model, document.body); } class ViewHelper { static setupStyle() { const style = document.createElement('style'); // TODO: fade out style.appendChild(document.createTextNode(` .ref-view { /* 照搬自 h.desktop.css */ background: #f0e0d6; border: 1px solid #000; position: relative; width: fit-content; } .ref-view[data-status="closed"] { display: none; } .ref-view[data-status="floating"] { position: absolute; z-index: 999; transition: opacity 100ms ease-in; } .ref-view[data-status="open"] { display: block; } .ref-view[data-status="collapsed"] { display: block; max-height: 80px; overflow: hidden; text-overflow: ellipsis; } /* https://stackoverflow.com/a/22809380 */ .ref-view[data-status="collapsed"]:before { content: ''; position: absolute; top: 30px; width: 100%; height: 50px; background: linear-gradient(#f0e0d600, #ffeeddff); z-index: 999; } .ref-view-pin { display: inline-block; transform: rotate(-45deg); } /* https://codemyui.com/grayscale-emoji-using-css/ */ .ref-view[data-status="floating"] >.ref-view-item-container >.h-threads-item >.h-threads-item-ref >.h-threads-item-reply-main >.h-threads-info >.ref-view-pin { transform: none; filter: grayscale(100%); } `)); document.getElementsByTagName('head')[0].appendChild(style); } /** * * @param {Model} model * @param {HTMLElement} root */ static setupContent(model, root) { const po = ViewHelper.po; if (root !== document.body) { // 补标 PO root.querySelectorAll('.h-threads-info-uid').forEach((elem) => { if (ViewHelper.getPosterID(elem.parentNode) === po) { const poLabel = document.createElement('span'); poLabel.textContent = "(PO主)"; poLabel.classList.add('uk-text-primary', 'uk-text-small'); Utils.insertAfter(elem, poLabel); Utils.insertAfter(elem, document.createTextNode(' ')); } }); // 图钉📌按钮和刷新🔄按钮 root.querySelectorAll('.h-threads-info').forEach((parentElem) => { const pinSpan = document.createElement('span'); pinSpan.classList.add('ref-view-pin'); pinSpan.textContent = "📌"; pinSpan.addEventListener('click', (el) => { const viewDiv = pinSpan.closest('.ref-view'); const linkElem = viewDiv.parentNode.querySelector('.ref-link'); if (viewDiv.dataset.status === 'floating') { linkElem.dataset.status = 'open'; viewDiv.dataset.status = 'open'; } else { linkElem.dataset.status = 'closed'; viewDiv.dataset.status = 'closed'; } }); // const refreshSpan = document.createElement('span'); // refreshSpan.classList.add('ref-view-refresh'); // refreshSpan.textContent = "🔄"; parentElem.prepend(pinSpan, /*refreshSpan*/); }); } root.querySelectorAll('font[color="#789922"]').forEach(linkElem => { linkElem.classList.add('ref-link'); // closed: 无固定显示 view; open: 有固定显示 view linkElem.dataset.status = 'closed'; const r = /^>>No.(\d+)$/.exec(linkElem.textContent); if (!r) { return; } const refID = Number(r[1]); linkElem.dataset.refID = String(refID); const viewID = Utils.generateRandomID(); linkElem.dataset.viewID = viewID; const viewDiv = document.createElement('div'); viewDiv.classList.add('ref-view'); // closed: 不显示; floating: 悬浮显示; open: 完整固定显示; collapsed: 折叠固定显示 viewDiv.dataset.status = 'closed'; viewDiv.dataset.viewID = viewID; const itemContainer = document.createElement('div'); itemContainer.classList.add('ref-view-item-container'); viewDiv.appendChild(itemContainer); Utils.insertAfter(linkElem, viewDiv); // 处理悬浮 linkElem.addEventListener('mouseenter', (ev) => { if (viewDiv.dataset.status !== 'closed') { viewDiv.dataset.isHovering = '1'; return; } viewDiv.dataset.status = 'floating'; viewDiv.dataset.isHovering = '1'; this.doLoadViewContent(model, viewDiv, refID); }); viewDiv.addEventListener('mouseenter', () => { viewDiv.dataset.isHovering = '1'; }) for (const elem of [linkElem, viewDiv]) { elem.addEventListener('mouseleave', () => { if (viewDiv.dataset.status != 'floating') { return; } delete viewDiv.dataset.isHovering; (async () => { setTimeout(() => { if (!viewDiv.dataset.isHovering) { viewDiv.dataset.status = 'closed'; } }, 200); })(); }); } // 处理折叠 linkElem.addEventListener('click', () => { if (linkElem.dataset.status === 'closed' || viewDiv.dataset.status === 'collapsed') { linkElem.dataset.status = 'open'; viewDiv.dataset.status = 'open'; } else { viewDiv.dataset.status = 'collapsed'; } }); viewDiv.addEventListener('click', () => { if (viewDiv.dataset.status === 'collapsed') { viewDiv.dataset.status = 'open'; } }); }); } /** * * @param {Model} model * @param {HTMLElement} viewDiv * @param {number} refID */ static doLoadViewContent(model, viewDiv, refID) { const viewID = viewDiv.dataset.viewID; // TODO: 更好的「加载中」 viewDiv.classList.add('ref-view-loading'); const itemContainer = viewDiv.getElementsByClassName('ref-view-item-container')[0]; itemContainer.textContent = "加载中…"; (async (model) => { const itemElement = await model.loadItemElement(refID, viewID); viewDiv.classList.remove('ref-view-loading'); itemContainer.innerHTML = ''; // console.log(itemElement); itemContainer.appendChild(itemElement); })(model); } static get po() { return ViewHelper.getPosterID(document.querySelector('.h-threads-item-main')); } /** * * @param {HTMLElement} elem */ static getPosterID(elem) { const uid = elem.querySelector('.h-threads-info-uid').textContent; return /^ID:(.*)$/.exec(uid)[1]; } } class Model { constructor() { this.viewCache = {}; this.refCache = {}; } get isSupported() { if (!window.indexedDB || !window.fetch) { return false; } return true; } // TODO: indexedDB 持久化数据 /** * * @param {String} viewID * @returns {HTMLElement?} */ async getViewCache(viewID) { return this.viewCache[viewID]; } /** * * @param {String} viewID * @param {HTMLElement} item */ async recordView(viewID, item) { this.viewCache[viewID] = item; } /** * * @param {number} refID * @returns {HTMLElement?} */ async getRefCache(refID) { const elem = this.refCache[refID]; if (!elem) { return null; } return elem.cloneNode(true); } /** * * @param {number} refID * @param {HTMLElement} rawItem */ async recordRef(refID, rawItem) { this.refCache[refID] = rawItem.cloneNode(true); } /** * * @param {number} refID * @param {String} viewID */ async loadItemElement(refID, viewID) { { const viewItemCache = await this.getViewCache(viewID); if (viewItemCache) { return viewItemCache; } } const itemContainer = document.createElement('div'); const itemCache = await this.getRefCache(refID); if (itemCache) { itemContainer.appendChild(itemCache); } else { // TODO: timeout 20s try { const resp = await fetch(`/Home/Forum/ref?id=${refID}`); itemContainer.innerHTML = await resp.text(); } catch (e) { // TODO: 异常处理 console.log(e); itemContainer.innerHTML = "获取引用内容失败"; return itemContainer.firstChild; } } const item = itemContainer.firstChild; console.log(item); this.recordRef(refID, item); ViewHelper.setupContent(this, item); this.recordView(item); return item; } } class Utils { // https://stackoverflow.com/a/59837035 static generateRandomID() { return Math.random().toString(36).replace('0.', ''); } /** * * @param {Node} node * @param {Node} newNode */ static insertAfter(node, newNode) { node.parentNode.insertBefore(newNode, node.nextSibling); } } entry(); })();