// ==UserScript==
// @name 论坛列表显示图片
// @namespace form_show_images_in_list
// @version 1.1
// @description 论坛列表显示图片,支持phpwind和discuz
// @license MIT
// @author Gloduck
// @match */forum.php?mod=forumdisplay*
// @match */thread.php?fid*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
GM_addStyle(`
.zoomable-image {
cursor: pointer;
}
.zoomable-image.zoomed {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
background: rgba(0, 0, 0, 0.9);
z-index: 9999;
}
`);
// todo 添加extend继承的功能
let settings = [
{
name: "discuz",
articleListSelector: 'tbody[id^="normalthread_"]',
postContentSelector: 'td[id^="postmessage_"]',
ignoreImageRegs: [
"/uc_server/images/*",
"static/image/*",
"/uc_server/data/avatar/*"
],
maxShowLimit: 3,
lazyLoad: true,
postImageLinkCallback: function (element) {
let fileLink = element.getAttribute('file');
if (fileLink) {
return fileLink;
}
return element.getAttribute('src');
},
initElementDecorator: function (element) {
let tbody = document.createElement("tbody");
let tr = document.createElement("tr");
tr.appendChild(element);
tbody.appendChild(tr);
return tbody;
}
},
{
name: "phpwind",
articleListSelector: '#ajaxtable tbody:last-of-type tr[align=center]',
postContentSelector: '#read_tpc',
ignoreImageRegs: [],
maxShowLimit: 3,
lazyLoad: true,
postImageLinkCallback: function (element) {
return element.getAttribute('src');
},
initElementDecorator: function (element) {
let tr = document.createElement("tr");
tr.align = "center";
let td = document.createElement("td");
td.colSpan = 5;
tr.appendChild(td);
td.appendChild(element);
return tr;
}
}
]
let urlMatchers = [
{
name: "discuz",
urlMatch: [
"forum\\.php\\?mod=forumdisplay"
]
},
{
name: "phpwind",
urlMatch: [
"thread\\.php\\?fid"
]
}
]
activeByUrl();
function activeByUrl() {
let activeSetting = "";
for (let urlMatcher of urlMatchers) {
let urlRegs = regStrToReg(urlMatcher.urlMatch);
if (checkRegMatchStr(urlRegs, window.location.href)) {
activeSetting = urlMatcher.name;
break;
}
}
if (!activeSetting) {
console.log("无法找到要激活的配置");
} else {
console.log("激活的配置为:" + activeSetting);
showImageInList(activeSetting);
}
}
function showImageInList(type) {
let setting = getSettingByType(type);
let articleListElement = document.querySelectorAll(setting.articleListSelector);
articleListElement.forEach(element => {
if (setting.lazyLoad) {
lazyLoadImageInList(element, setting);
} else {
loadImageInList(element, setting);
}
})
}
function lazyLoadImageInList(element, setting){
// 注册(不可用)滚动事件,实现懒加载。同时通过节流来避免重复加载
window.addEventListener('scroll', throttle(function () {
const targetElementRect = element.getBoundingClientRect();
if (targetElementRect.top < window.innerHeight && !element.getAttribute("imageLoad")) {
handleSingleArticle(element, setting).then(toAppendElement => {
if (!element.getAttribute("imageLoad")) {
insertElementBelow(element, toAppendElement);
element.setAttribute("imageLoad", "true");
}
})
}
}, 200, 500));
}
function loadImageInList(element, setting){
handleSingleArticle(element, setting).then(toAppendElement => {
insertElementBelow(element, toAppendElement);
})
}
function insertElementBelow(targetElement, newElement) {
var parentElement = targetElement.parentNode;
parentElement.insertBefore(newElement, targetElement.nextSibling);
}
function getSettingByType(type) {
let setting = settings.find(value => {
return value.name === type;
});
if (setting == null) {
throw new Error("不支持的类型");
}
return setting;
}
/**
* 处理单个文章,返回最后需要拼接的element
* @param element {Element}
* @returns {Promise<void>}
*/
async function handleSingleArticle(element, setting) {
if (!element) {
throw new Error("参数不能为空");
}
let link = findFirstAnchorLink(element);
if (!link) {
throw new Error("找不到链接");
}
let postResult = await httpRequest("GET", "/" + link);
if (!postResult) {
throw new Error("请求文章错误");
}
var htmlDivElement = document.createElement("div");
// 初始化图片区域
htmlDivElement.appendChild(getImagesDiv(setting, postResult));
// todo 添加自定义元素
return setting.initElementDecorator(htmlDivElement);
}
function getImagesDiv(setting, content) {
let images = parsePostImages(setting, content);
if (setting.maxShowLimit && setting.maxShowLimit > 0) {
images = images.slice(0, setting.maxShowLimit);
}
let imageDiv = document.createElement("div");
imageDiv.style = "display: flex;";
imageDiv.className = "image_list";
images.forEach(value => {
let imgElement = document.createElement("img");
imgElement.src = value;
imgElement.style = "max-width: 300px;max-height: 300px;margin-right: 10px"
imageDiv.appendChild(imgElement);
imgElement.addEventListener('click', function () {
// 创建一个新的图片元素
var zoomedImg = document.createElement('img');
zoomedImg.src = imgElement.src;
// 添加类名以应用放大样式
zoomedImg.classList.add('zoomable-image', 'zoomed');
// 点击放大的功能
zoomedImg.addEventListener('click', function () {
// 移除放大的图片元素
document.body.removeChild(zoomedImg);
});
// 将放大的图片元素添加到文档中
document.body.appendChild(zoomedImg);
});
})
return imageDiv;
}
/**
*
* @param setting
* @param postDetails {string}
*/
function parsePostImages(setting, postDetails) {
let images = [];
let content = new DOMParser().parseFromString(postDetails, "text/html");
if (!content) {
return images;
}
let postContentSelector = setting.postContentSelector;
let postContent = content.querySelector(postContentSelector);
if (!postContent) {
return images;
}
let ignoreImageRegs = regStrToReg(setting.ignoreImageRegs);
let imageElements = postContent.querySelectorAll('img');
imageElements.forEach(imageElement => {
let imageLink = setting.postImageLinkCallback(imageElement);
if (checkRegMatchStr(ignoreImageRegs, imageLink)) {
return;
}
images.push(imageLink);
})
return images;
}
function findFirstAnchorLink(element) {
const linkElement = element.querySelector("a");
if (linkElement) {
return linkElement.getAttribute("href");
} else {
const childElements = element.children;
for (let i = 0; i < childElements.length; i++) {
const link = findFirstAnchorLink(childElements[i]);
if (link) {
return link;
}
}
}
return null;
}
function regStrToReg(regs) {
return regs.map(value => {
return new RegExp(value);
});
}
function checkRegMatchStr(regs, content) {
if (!content || !regs) {
throw new Error("参数不能为空");
}
for (var i = 0; i < regs.length; i++) {
if (regs[i].test(content)) {
return true;
}
}
return false;
}
/**
* 防抖
* @param func
* @param wait
* @param immediate
* @returns {(function(): void)|*}
*/
function debounce(func, wait, immediate) {
// 定时器变量
var timeout;
return function () {
// 每次触发 scroll handler 时先清除定时器
clearTimeout(timeout);
// 指定 xx ms 后触发真正想进行的操作 handler
timeout = setTimeout(func, wait);
};
};
/**
* 节流
* @param func
* @param wait
* @param mustRun
* @returns {(function(): void)|*}
*/
function throttle(func, wait, mustRun) {
var timeout,
startTime = new Date();
return function () {
var context = this,
args = arguments,
curTime = new Date();
clearTimeout(timeout);
// 如果达到了规定的触发时间间隔,触发 handler
if (curTime - startTime >= mustRun) {
func.apply(context, args);
startTime = curTime;
// 没达到触发间隔,重新设定定时器
} else {
timeout = setTimeout(func, wait);
}
};
};
function httpRequest(method, url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
onload: function (response) {
resolve(response.responseText);
},
onerror: function (error) {
reject(error);
}
});
});
}
})();