Kemono 使用增強

側邊欄收縮美化界面 , 自動加載大圖 , 簡易隱藏廣告 , 翻頁優化 , 自動開新分頁

Tính đến 17-08-2023. Xem phiên bản mới nhất.

// ==UserScript==
// @name         Kemono 使用增強
// @name:zh-TW   Kemono 使用增強
// @name:zh-CN   Kemono 使用增强
// @name:ja      Kemono 使用を強化
// @name:en      Kemono Usage Enhancement
// @version      0.0.23
// @author       HentiSaru
// @description        側邊欄收縮美化界面 , 自動加載大圖 , 簡易隱藏廣告 , 翻頁優化 , 自動開新分頁
// @description:zh-TW  側邊欄收縮美化界面 , 自動加載大圖 , 簡易隱藏廣告 , 翻頁優化 , 自動開新分頁
// @description:zh-CN  侧边栏收缩美化界面 , 自动加载大图 , 简易隐藏广告 , 翻页优化 , 自动开新分页
// @description:ja     サイドバーの収縮によるインターフェースの美化、大画像の自動読み込み、広告の簡易非表示、ページめくりの最適化、新しいページの自動開封
// @description:en     Sidebar contraction beautifies interface, automatically loads large images, easily hides ads, optimizes paging, and automatically opens new tabs.

// @match        *://kemono.su/*
// @match        *://*.kemono.su/*
// @match        *://kemono.party/*
// @match        *://*.kemono.party/*
// @icon         https://cdn-icons-png.flaticon.com/512/2566/2566449.png

// @license      MIT
// @namespace    https://greasyfork.org/users/989635

// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_addElement
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText

// @require      https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
// @resource     font-awesome https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css
// @require      https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js
// ==/UserScript==

var xhr = new XMLHttpRequest(),
    Url = window.location.href,
    parser = new DOMParser(),
    buffer = document.createDocumentFragment();
(function () {
    let interval, tryerror = 0, dellay = 300;
    const pattern = /^(https?:\/\/)?(www\.)?kemono\..+\/.+\/user\/.+\/post\/.+$/,
        UserPage = /^(https?:\/\/)?(www\.)?kemono\..+\/.+\/user\/[^\/]+(\?.*)?$/,
        PostsPage = /^(https?:\/\/)?(www\.)?kemono\..+\/posts\/?(\?.*)?$/,
        DmsPage = /^(https?:\/\/)?(www\.)?kemono\..+\/dms\/?(\?.*)?$/;
    async function Main() {
        const [list, box, comments, announce] = [ // comments(評論區標題), announce(公告條)
            "div.global-sidebar", "div.content-wrapper.shifted", "h2.site-section__subheading", "body > div.content-wrapper.shifted > a"
        ].map(selector => document.querySelector(selector));
        if ((box && list && comments) || (box && list)) {
            Beautify(box, list, announce); // 側邊欄收縮
            if (pattern.test(Url)) { Additional(comments) }// (帖子內) Ajex 快捷換頁
            clearInterval(interval);
        } else {
            tryerror++;
            if (tryerror > 10) { clearInterval(interval) }
        }
    }
    interval = setInterval(() => { Main() }, dellay);
    setTimeout(() => {
        AdHiding(); // 隱藏廣告
        if (pattern.test(Url)) {
            OriginalImage(); // 自動大圖
            LinkOriented(); // 連結轉換
            VideoBeautify(); // 影片美化
        }
        if (UserPage.test(Url) || PostsPage.test(Url) || DmsPage.test(Url)) {
            AjexPostToggle(); // Ajex 換頁
            NewTabOpens(); // 自動新分頁
        }
    }, dellay);
})();

/* ==================== */

/* 美化介面 */
async function Beautify(box, list, announce) {
    GM_addStyle(`
        .list_column {
            opacity: 0;
            width: 10rem !important;
            transform: translateX(-9rem);
            transition: 0.8s;
        }
        .list_column:hover {
            opacity: 1;
            transform: translateX(0rem);
        }
        .main_box {
            transition: 0.7s;
        }
    `);
    try {
        announce.remove();
        box.classList.add("main_box");
        box.style.marginLeft = "0rem";
        list.classList.add("list_column");
        list.addEventListener('mouseenter', function () {
            box.style.marginLeft = "10rem";
        });
        list.addEventListener('mouseleave', function () {
            box.style.marginLeft = "0rem";
        });
    } catch { }
}

async function VideoBeautify() {
    let stream, parents;
    parents = document.querySelectorAll('ul[style*="text-align: center;list-style-type: none;"] li');
    if (parents.length > 0) {
        function ReactBeautify({ stream }) {
            return React.createElement("video", {
                key: "video",
                controls: true,
                preload: "auto",
                style: { width: "80%", height: "80%" },
            }, React.createElement("source", {
                key: "source",
                src: stream.src,
                type: stream.type
            }));
        }
        parents.forEach(li => {
            stream = li.querySelector("source");
            if (stream) {
                ReactDOM.render(React.createElement(ReactBeautify, { stream: stream }), li);
            } else {
                console.log("Debug: Could not find source, please refresh");
            }
        })
    }
}

/* 載入原圖 */
async function OriginalImage() {
    GM_addStyle(`
        .img-style {
            max-width: 100%;
            display: block;
            margin: 0 auto;
        }
    `);
    let thumbnail, href, img;
    thumbnail = document.querySelectorAll("div.post__thumbnail");
    if (thumbnail.length > 0) {
        function ImgRendering({ ID, href }) {
            return React.createElement("a", {
                id: ID,
                className: "image-link"
            }, React.createElement("img", {
                key: "img",
                src: href.href,
                className: "img-style",
                onError: function () {
                    Reload(ID, 15);
                }
            })
            )
        }
        thumbnail.forEach(async (object, index) => {
            object.classList.remove("post__thumbnail");
            href = object.querySelector("a");
            await ReactDOM.render(React.createElement(ImgRendering, { ID: `IMG-${index}`, href: href }), object);
            await new Promise(resolve => setTimeout(resolve, 800));
        })
        document.querySelectorAll("a.image-link").forEach(link => {
            const handleClick = () => {
                img = link.querySelector("img");
                if (!img.complete) {
                    img.src = img.src;
                } else {
                    link.removeEventListener("click", handleClick);
                }
            }
            link.addEventListener("click", handleClick);
        });
    }
}
async function Reload(ID, retry) {
    if (retry > 0) {
        setTimeout(() => {
            let object = document.getElementById(ID), old = object.querySelector("img"), img = document.createElement("img");
            img.src = old.src;
            img.alt = "Click Reload";
            img.className = "img-style";
            img.onerror = function () { Reload(ID, retry) };
            old.remove();
            object.appendChild(buffer.appendChild(img));
            retry - 1;
        }, 1800);
    }
}

/* ==================== */

/* 監聽器的添加與刪除 */
var ListenerRecord = new Map(), listen;

async function addlistener(element, type, listener) {
    if (!ListenerRecord.has(element) || !ListenerRecord.get(element).has(type)) {
        element.addEventListener(type, listener);
        if (!ListenerRecord.has(element)) {
            ListenerRecord.set(element, new Map());
        }
        ListenerRecord.get(element).set(type, listener);
    }
}

async function removlistener(element, type) {
    if (ListenerRecord.has(element) && ListenerRecord.get(element).has(type)) {
        listen = ListenerRecord.get(element).get(type);
        element.removeEventListener(type, listen);
        ListenerRecord.get(element).delete(type);
    }
}

/* ==================== */

/* 簡易隱藏廣告 */
async function AdHiding() {
    document.querySelectorAll(".ad-container").forEach(function (element) {
        try { element.style.display = "none" } catch { element.style.visibility = "hidden" }
    })
    let attempts = 0, interval = setInterval(function () {
        if (attempts < 5) {
            document.querySelectorAll(".root--ujvuu").forEach((element) => {
                try {
                    element.style.opacity = 0;
                    element.style.visibility = "hidden";
                } catch {
                    element.style.visibility = "hidden";
                }
            });
            attempts++;
        } else { clearInterval(interval) }
    }, 700);
}

/* 轉換下載連結參數 */
async function LinkOriented() {
    document.querySelectorAll("a.post__attachment-link").forEach(link => {
        link.setAttribute("download", "");
    })
}

/* 底部按鈕創建, 監聽快捷Ajex換頁 */
async function Additional(comments) {
    GM_addStyle(GM_getResourceText("font-awesome"));
    const prev = document.querySelector("a.post__nav-link.prev");
    const next = document.querySelector("a.post__nav-link.next");
    const span = document.createElement("span");
    const svg = document.createElement("svg");
    span.style = "float: right";
    span.appendChild(next.cloneNode(true));
    svg.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512" style="margin-left: 10px;cursor: pointer;">
            <style>svg{fill:#e8a17d}</style>
            <path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM135.1 217.4l107.1-99.9c3.8-3.5 8.7-5.5 13.8-5.5s10.1 2 13.8 5.5l107.1 99.9c4.5 4.2 7.1 10.1 7.1 16.3c0 12.3-10 22.3-22.3 22.3H304v96c0 17.7-14.3 32-32 32H240c-17.7 0-32-14.3-32-32V256H150.3C138 256 128 246 128 233.7c0-6.2 2.6-12.1 7.1-16.3z"></path>
        </svg>
    `
    buffer.appendChild(svg);
    buffer.appendChild(span);
    comments.appendChild(buffer);
    addlistener(svg, "click", () => {
        document.querySelector("header").scrollIntoView();
    })

    // 監聽按鍵切換
    const main = document.querySelector("main");
    addlistener(document, "keydown", event => {
        try {
            if (event.key === "4") {
                event.preventDefault();
                removlistener(document, "keydown");
                AjexReplace(prev.href, main);
            } else if (event.key === "6") {
                event.preventDefault();
                removlistener(document, "keydown");
                AjexReplace(next.href, main);
            }
        } catch { }
    })
}

GM_addStyle(`
    .gif-overlay {
        position: absolute;
        opacity: 0.4;
        top: 50%;
        left: 50%;
        width: 70%;
        height: 70%;
        z-index: 9999;
        border-radius: 50%;
        transform: translate(-50%, -50%);
    }
    .diluted-information {
        opacity: 0.4;
    }
`);

/* 將瀏覽帖子頁面都變成開新分頁, 帖子說明文字淡化, 和滑鼠懸浮恢復 */
async function NewTabOpens() {
    const card = document.querySelectorAll("div.card-list__items article a");
    card.forEach(link => {
        link.querySelector("header").classList.add("diluted-information");
        link.querySelector("footer").classList.add("diluted-information");
        addlistener(link, "click", event => {
            event.preventDefault();
            GM_openInTab(link.href, { active: false, insert: true });
        })
        addlistener(link, "mouseenter", () => {
            link.querySelector("header").classList.remove("diluted-information");
            link.querySelector("footer").classList.remove("diluted-information");
        })
        addlistener(link, "mouseleave", () => {
            link.querySelector("header").classList.add("diluted-information");
            link.querySelector("footer").classList.add("diluted-information");
        })
    });
}

/* ==================== */

/* Ajex 替換頁面的初始化 */
async function Initialization() {
    let interval = setInterval(function () {
        const comments = document.querySelector("h2.site-section__subheading");
        if (comments) {
            Additional(comments);
            clearInterval(interval);
        }
    }, 300);
    setTimeout(OriginalImage, 500);
    setTimeout(VideoBeautify, 500);
    document.querySelector("h1.post__title").scrollIntoView(); // 滾動到上方
}

/* React 渲染優化 */
function ReactRendering({ content }) {
    return React.createElement("div", { dangerouslySetInnerHTML: { __html: content } });
}
async function AjexReplace(url, old_main) {
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            let New_data = parser.parseFromString(xhr.responseText, 'text/html');
            let New_main = New_data.querySelector("main");
            ReactDOM.render(React.createElement(ReactRendering, { content: New_main.innerHTML }), old_main);
            history.pushState(null, null, url);
            setTimeout(Initialization(), 500);
        }
    };
    xhr.open("GET", url, true);
    xhr.send();
}

/* 帖子切換 */
async function AjexPostToggle() {
    let Old_data, New_data, item;
    async function Request(link) {
        item = document.querySelector("div.card-list__items");
        item.style.position = "relative";
        GM_addElement(item, "img", {
            src: "https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/images/loading.gif",
            class: "gif-overlay"
        });
        GM_xmlhttpRequest({
            method: "GET",
            url: link,
            nocache: false,
            onload: response => {
                Old_data = document.querySelector("section");
                New_data = parser.parseFromString(response.responseText, "text/html").querySelector("section");
                ReactDOM.render(React.createElement(ReactRendering, { content: New_data.innerHTML }), Old_data);
                history.pushState(null, null, link);
                AjexPostToggle();
                NewTabOpens();
                AdHiding();
            }
        });
    }
    try {
        const menu = document.querySelectorAll("menu a");
        menu.forEach(ma => {
            addlistener(ma, "click", (event) => {
                event.preventDefault();
                Request(ma.href);
            })
        });
    } catch {}
}