Greasy Fork is available in English.

论坛列表显示图片

论坛列表显示图片,同时支持discuz搭建的论坛(如吾爱破解)以及phpwind搭建的论坛(如south plus)灯

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         论坛列表显示图片
// @namespace    form_show_images_in_list
// @version      1.4
// @description  论坛列表显示图片,同时支持discuz搭建的论坛(如吾爱破解)以及phpwind搭建的论坛(如south plus)灯
// @license MIT
// @author       Gloduck
// @note         discuz路径匹配
// @match        *://*/forum-*.html
// @match        *://*/forum-*.html?*
// @match        *://*/forum.php
// @match        *://*/forum.php?*
// @match        *://*/*/forum-*.html
// @match        *://*/*/forum-*.html?*
// @match        *://*/*/forum.php
// @match        *://*/*/forum.php?*
// @note         phpwind路径匹配
// @match        *://*/*/thread.php
// @match        *://*/*/thread.php?*
// @match        *://*/thread.php
// @match        *://*/thread.php?*
// @note         1024路径匹配
// @match        *://*/*/thread0806.php*
// @match        *://*/thread0806.php*
// @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;
        }
    `);


    let typeHandlers = [
        {
            // 类型名称
            name: "discuz",
            // 文章列表选择器
            articleListSelector: 'tbody[id^="normalthread_"]',
            // 文章链接a标签选择器
            articleLinkSelector: '.icn a',
            // 文章详情页中文章主体选择器
            postContentSelector: 'div[id^="post_"] .plc',
            // 找到img标签后,解析img标签中链接的callback
            postImageLinkCallback: function (element) {
                let fileLink = element.getAttribute('file');
                if (fileLink) {
                    return fileLink;
                }
                return element.getAttribute('src');
            },
            // 初始化好放图片的div后,对该div的element包装
            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]',
            articleLinkSelector: 'td a',
            postContentSelector: '.tpc_content',
            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;
            }
        },
        {
            name: "1024",
            articleListSelector: 'tbody[id="tbody"] tr',
            articleLinkSelector: '.tal h3 a',
            postContentSelector: '#conttpc',
            postImageLinkCallback: function (element) {
                let fileLink = element.getAttribute('ess-data');
                if (fileLink) {
                    return fileLink;
                }
                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 urlPatterns = [
        {
            name: "discuz",
            pattern: [
                "*://*/forum-*.html",
                "*://*/forum-*.html?*",
                "*://*/forum.php",
                "*://*/forum.php?*",
                "*://*/*/forum-*.html",
                "*://*/*/forum-*.html?*",
                "*://*/*/forum.php",
                "*://*/*/forum.php?*"
            ]
        },
        {
            name: "phpwind",
            pattern: [
                "*://*/*/thread.php",
                "*://*/*/thread.php?*",
                "*://*/thread.php",
                "*://*/thread.php?*"
            ]
        },
        {
            name: "1024",
            pattern: [
                "*://*/*/thread0806.php*",
                "*://*/thread0806.php*"
            ]
        }
    ]

    // todo 添加自定义设置功能,添加插件功能
    // 类型的基础设置,勿动
    let typeBaseSettings = [
        {
            pattern: "discuz",
            lazyLoad : true,
            maxShowLimit : 3,
            ignoreImageRegs: [
                "/uc_server/images/*",
                "static/image/*",
                "/data/avatar/*"
            ],
            plugins: []
        },
        {
            pattern: "phpwind",
            lazyLoad : true,
            maxShowLimit : 3,
            ignoreImageRegs: [
                "images/post/smile/*",
            ],
            plugins: []

        },
        {
            pattern: "1024",
            lazyLoad : true,
            maxShowLimit : 3,
            ignoreImageRegs: [
                "https://23img.com/*",
                "https://img.blr844.com/images/.*.gif",
                "https://avspda.xyz/*",
            ],
            plugins: []
        }
    ];

    activeByUrlPattern();


    function activeByUrlPattern() {
        let activeSettingNames = [];
        urlPatterns.forEach(value => {
            let urlPatternReg = value.pattern.map(urlPattern => toURlPattern(urlPattern));
            if (checkRegMatchStr(urlPatternReg, window.location.href)) {
                activeSettingNames.push(value.name);
            }
        })
        if (activeSettingNames.length == 0) {
            console.log("无法找到要激活的配置");
            return;
        }
        if (activeSettingNames.length != 1) {
            console.log("找到多个匹配的配置,默认激活第一个")
        }
        let activeSettingName = activeSettingNames[0];
        console.log("激活的配置为:" + activeSettingName);
        enhancementByType(activeSettingName);
    }

    /**
     * 根据当前的host获取用户自定义的配置
     * @param type
     * @returns {Object|{}}
     */
    function getCustomTypeSettingOfHost(type){
        return getCustomTypeSettingOrDefault(window.location, type);
    }

    /**
     * 获取用户自定义的配置或者默认的配置
     * @param pattern {string}
     * @param type {string}
     * @returns {Object|{}}
     */
    function getCustomTypeSettingOrDefault(pattern, type){
        let typeBaseSetting = typeBaseSettings.find(p => p.pattern === type);
        if(!typeBaseSetting){
            throw new Error(`类型[${type}],无法找到默认的配置`);
        }
        // 深拷贝对象,防止基本设置被更改
        typeBaseSetting = deepCopy(typeBaseSetting);
        let customSetting = typeBaseSettings.find(p => p.pattern == pattern);
        if(!customSetting){
            // 如果没有用户自定义的设置,则返回默认设置的深拷贝
            return typeBaseSetting;
        }
        customSetting = deepCopy(customSetting);
        // 添加缺失的属性到用户自定义的设置里,兼容升级
        addMissingProperties(customSetting, typeBaseSetting);
        customSetting.pattern = pattern;
        return customSetting;
    }

    /**
     * 根据类型来选择增强的处理程序
     * @param type {string} 类型
     */
    function enhancementByType(type) {
        let typeHandler = getSettingByType(type);
        let typeSetting = getCustomTypeSettingOfHost(type);
        let articleListElement = document.querySelectorAll(typeHandler.articleListSelector);
        articleListElement.forEach(element => {
            if (typeSetting.lazyLoad) {
                lazyEnhancement(element, typeHandler, typeSetting);
            } else {
                immediateEnhancement(element, typeHandler, typeSetting);
            }
        })
    }

    /**
     * 懒增强
     * @param element {Element}
     * @param typeHandler {Object}
     * @param typeSetting {Object}
     */
    function lazyEnhancement(element, typeHandler, typeSetting) {
        // 注册滚动事件,实现懒加载。同时通过节流来避免重复加载
        window.addEventListener('scroll', throttle(function () {
            const targetElementRect = element.getBoundingClientRect();
            if (targetElementRect.top < window.innerHeight && !element.getAttribute("has_enhanced")) {
                handleSingleArticle(element, typeHandler, typeSetting).then(toAppendElement => {
                    if (!element.getAttribute("has_enhanced")) {
                        insertElementBelow(element, toAppendElement);
                        element.setAttribute("has_enhanced", "true");
                    }
                })
            }
        }, 200, 500));
    }

    /**
     * 立即增强
     * @param element {Element}
     * @param typeHandler {Object}
     * @param typeSetting {Object}
     */
    function immediateEnhancement(element, typeHandler, typeSetting) {
        handleSingleArticle(element, typeHandler, typeSetting).then(toAppendElement => {
            insertElementBelow(element, toAppendElement);
        })
    }

    /**
     * 插入元素到对应元素之后(需要有夫元素)
     * @param targetElement {Element}
     * @param newElement {Element}
     */
    function insertElementBelow(targetElement, newElement) {
        var parentElement = targetElement.parentNode;
        parentElement.insertBefore(newElement, targetElement.nextSibling);
    }


    /**
     * 根据类型获取设置信息
     * @param type
     * @return {Object}
     */
    function getSettingByType(type) {
        let typeHandler = typeHandlers.find(value => {
            return value.name === type;
        });
        if (typeHandler == null) {
            throw new Error("不支持的类型");
        }
        return typeHandler;
    }

    /**
     * 处理单个文章,返回最后需要拼接的element
     * @param element {Element}
     * @param typeHandler {Object}
     * @param typeSetting {Object}
     * @returns {Promise<void>}
     */
    async function handleSingleArticle(element, typeHandler, typeSetting) {
        if (!element) {
            throw new Error("参数不能为空");
        }
        let link = findActualArticleLinkBySelector(typeHandler.articleLinkSelector, element);
        let postResult = await httpRequest("GET", link);
        if (!postResult) {
            throw new Error("请求文章错误");
        }
        var htmlDivElement = document.createElement("div");
        // 初始化图片区域
        htmlDivElement.appendChild(getImagesDiv(typeHandler, typeSetting, link, postResult));
        return typeHandler.initElementDecorator(htmlDivElement);
    }

    /**
     * 根据设置,解析文章中的图片,并且生成html div
     * @param typeHandler {Object}
     * @param content {string}
     * @param postLink {string}
     * @param limitCount {number}
     * @returns {HTMLDivElement}
     */
    function getImagesDiv(typeHandler, typeSetting, postLink, content) {
        let images = parsePostImages(typeHandler, postLink, content, typeSetting.ignoreImageRegs);
        if (typeSetting.maxShowLimit && typeSetting.maxShowLimit > 0) {
            images = images.slice(0, typeSetting.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 typeHandler {Object} 类型设置
     * @param postLink {string} 文章链接
     * @param postDetails {string} 文章的内容字符串(解析前的html)
     * @param ignoreRegStrs {string} 忽略图片的正则表达式(解析前)
     * @returns {*[]}
     */
    function parsePostImages(typeHandler, postLink, postDetails, ignoreRegStrs) {
        let images = [];
        let content = new DOMParser().parseFromString(postDetails, "text/html");
        if (!content) {
            return images;
        }
        let postContentSelector = typeHandler.postContentSelector;
        let postContent = content.querySelector(postContentSelector);
        if (!postContent) {
            console.log("无法匹配到文章主体,请确认选择器是否正确,并确认点击链接进去是否能正常访问内容,匹配失败的链接为:" + postLink);
            return images;
        }
        let ignoreImageRegs = regStrToReg(ignoreRegStrs);
        let imageElements = postContent.querySelectorAll('img');
        imageElements.forEach(imageElement => {
            let imageLink = typeHandler.postImageLinkCallback(imageElement);
            if (checkRegMatchStr(ignoreImageRegs, imageLink)) {
                return;
            }
            images.push(convertPathToAccessible(imageLink, postLink));
        })
        return images;
    }

    /**
     * 通过文章链接选择器获取文章的绝对链接
     * @param selector {string}
     * @param element {Element}
     */
    function findActualArticleLinkBySelector(selector, element) {
        let linkElement = element.querySelector(selector);
        if (!linkElement) {
            throw new Error("通过选择器,无法找到文章的链接元素");
        }
        let href = linkElement.getAttribute("href");
        if (!href) {
            throw new Error("无法获取href元素,请确认选择器是否最终选择了一个a标签,以及a标签上是否有href");
        }
        return convertPathToAccessible(href, window.location.href);
    }

    /**
     * 找到第一个a标签中的链接
     * @param element {Element}
     * @returns {*|string|null}
     */
    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;
    }

    /**
     * 正则表达式字符串列表转正则表达式列表
     * @param regs {string[]}
     * @returns {*}
     */
    function regStrToReg(regs) {
        return regs.map(value => {
            return new RegExp(value);
        });
    }

    /**
     * 校验正则表达式是否匹配内容
     * @param regs {RegExp[]}
     * @param content {string}
     * @returns {boolean}
     */
    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;
    }

    function convertPathToAccessible(path, currentPath) {
        var url = new URL(path, currentPath);
        return url.href;
    }

    /**
     * 防抖
     * @param func {function} 回调函数
     * @param wait 等待时间(ms)
     * @returns {(function(): void)|*}
     */
    function debounce(func, wait) {
        // 定时器变量
        var timeout;
        return function () {
            // 每次触发 scroll handler 时先清除定时器
            clearTimeout(timeout);
            // 指定 xx ms 后触发真正想进行的操作 handler
            timeout = setTimeout(func, wait);
        };
    };

    /**
     * 节流
     * @param func {function} 回调函数
     * @param wait 延迟执行时间(ms)
     * @param mustRun 必须执行时间(ms)
     * @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);
            }
        };
    };


    /**
     * @param patternStr {string}
     * @returns {RegExp}
     */
    function toURlPattern(patternStr) {
        return new RegExp('^' + patternStr
            .replace(/\*/g, '.*')
            .replace(/\//g, '\\/'));
    }

    /**
     * 调用油猴脚本发送请求
     * @param method {string} 请求方式
     * @param url {string} 请求地址
     * @returns {Promise<unknown>}
     */
    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);
                }
            });
        });
    }

    /**
     * 深拷贝对象
     * @param obj {Object}
     * @returns {Object}
     */
    function deepCopy(obj) {
        if (typeof obj !== 'object' || obj === null) {
            return obj; // 如果是基本数据类型或 null,则直接返回
        }

        let copy;
        if (Array.isArray(obj)) {
            copy = [];
            for (let i = 0; i < obj.length; i++) {
                copy[i] = deepCopy(obj[i]); // 递归复制数组中的每个元素
            }
        } else {
            copy = {};
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    copy[key] = deepCopy(obj[key]); // 递归复制对象中的每个属性
                }
            }
        }
        return copy;
    }

    /**
     * 添加缺少的属性
     * @param target {Object}
     * @param source {Object}
     */
    function addMissingProperties(target, source) {
        for (let key in source) {
            if (!target.hasOwnProperty(key)) {
                target[key] = source[key];
            }
        }
    }



})();