Kemono/Coomer grid layout for post

將文章中的圖片改為網格顯示,並提供幻燈片檢視

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name Kemono/Coomer grid layout for post
// @name:zh-TW Kemono/Coomer 貼文網格佈局
// @name:ja Kemono/Coomer 投稿グリッドレイアウト
// @name:en Kemono/Coomer grid layout for post
// @name:de Kemono/Coomer Raster-Layout für Beiträge
// @name:es Diseño en cuadrícula para publicaciones de Kemono/Coomer
// @description 將文章中的圖片改為網格顯示,並提供幻燈片檢視
// @description:zh-TW 將貼文內的圖片改為網格排列,並提供全螢幕幻燈片檢視功能
// @description:ja 投稿内の画像をグリッド表示に変更し、全画面スライドショー閲覧機能を追加します
// @description:en Changes images in posts to a clean grid layout and adds fullscreen slideshow viewing
// @description:de Ändert Bilder in Beiträgen in ein übersichtliches Raster-Layout und fügt Vollbild-Diashow hinzu
// @description:es Cambia las imágenes de las publicaciones a un diseño en cuadrícula limpio y añade modo presentación a pantalla completa
//
// @version 1.0.12
// @match https://kemono.cr/*/user/*/post/*
// @match https://coomer.st/*/user/*/post/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_getValues
// @grant GM_registerMenuCommand
//
// @author Max
// @namespace https://github.com/Max46656
// @license MPL2.0
// ==/UserScript==

class ImageGridEnhancer {
    constructor() {
        this.settings = {
            gridColumns: GM_getValue("gridColumns", 3),
            slideshowSize: GM_getValue("slideshowSize", "large"),
            autoSlideshow: GM_getValue("autoSlideshow", false)
        };
        this.images = [];
        this.container = null;
        this.fullScreenContainer = null;
        this.currentIndex = 0;
        this.observeDOM();
        this.tidyUpPostImage();
        GM_registerMenuCommand("圖片網格設定", () => this.openSettingsPanel(), "G");
    }

    async tidyUpPostImage() {
        try{
            const postFiles = await this.waitForElement("div.post__files");
            const hadGrid = postFiles.classList.contains("image__grid");
            if (hadGrid && postFiles.style.gridTemplateColumns === `repeat(${GM_getValue("gridColumns")}, 1fr)`) return;
            this.container = postFiles;
            const divs = postFiles.querySelectorAll("div");
            if (divs.length === 0) return;
            this.container.style.position = "relative";
            this.container.style.padding = "12px 12px 8px";
            if (!hadGrid) {
                postFiles.classList.add("image__grid");
            }
            postFiles.innerHTML = "";
            postFiles.style.display = "grid";
            postFiles.style.gridTemplateColumns = `repeat(${this.settings.gridColumns}, 1fr)`;
            postFiles.style.gap = "10px";
            postFiles.style.marginTop = "8px";
            divs.forEach((div, index) => {
                const img = div.querySelector("img");
                if(!img) return;
                div.style.cssText = "";
                Object.assign(div.style, {
                    margin: "0",
                    overflow: "hidden",
                    borderRadius: "12px",
                    boxShadow: "0 4px 12px rgba(0,0,0,0.25)",
                    cursor: this.settings.autoSlideshow ? "zoom-in" : "pointer",
                    transition: "transform 0.2s ease, box-shadow 0.3s ease"
                });
                Object.assign(img.style, {
                    width: "100%",
                    height: "100%",
                    objectFit: "cover",
                    display: "block",
                    transition: "transform 0.35s ease"
                });
                div.onclick = (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    if (this.settings.autoSlideshow) {
                        this.openSlideshow(index);
                    }
                };
                postFiles.appendChild(div);
            });
            this.createSlideshowButton();
            this.deleteEmptyContainer();
        }catch(e){console.error(e)}
    }

    createSlideshowButton() {
        if (document.getElementById("gridBtn")) return;

        const btn = document.createElement("button");
        btn.id = "gridBtn";
        btn.textContent = "幻燈片模式";
        btn.title = "全螢幕幻燈片模式";

        btn.onclick = () => this.openSlideshow();

        Object.assign(btn.style, {
            position: "fixed",
            bottom: "20px",
            right: "20px",
            zIndex: "99999",
            padding: "6px 10px",
            backgroundColor: "#282a2e",
            color: "#e8a17d",
            border: "2px solid #3b3e44",
            borderRadius: "12px",
            cursor: "pointer",
            fontSize: "15px",
            fontWeight: "bold",
            boxShadow: "0 8px 20px rgba(0,0,0,0.4)",
            transition: "all 0.3s ease",
            userSelect: "none",
            backdropFilter: "blur(10px)"
        });

        document.body.appendChild(btn);
    }

    deleteEmptyContainer(){
        const container = document.querySelectorAll("div.post__files div");
        let count = 0;
        container.forEach((div)=>{
            if(div.querySelectorAll("img").length === 0) {
                div.remove();
                count++;
            }
        });
        if(count > 0) console.log(`已刪除${count}個空div`);
    }

    openSlideshow(startIndex = 0) {
        const thumbnails = Array.from(
            document.querySelectorAll("div.post__files div")
        );

        if (thumbnails.length === 0) return;

        this.currentIndex = Math.max(0, Math.min(startIndex, thumbnails.length - 1));

        const imageUrls = thumbnails.map(thumb => {
            const img = thumb.querySelector("img");
            if (!img) return null;
            let src = img.parentElement.href || img.src;
            if (src && !src.startsWith("http")) {
                src = new URL(src, location.origin).href;
            }
            return src;
        }).filter(Boolean);

        if (imageUrls.length === 0) return;

        if (!this.fullScreenContainer) {
            this.fullScreenContainer = document.createElement("div");
            this.fullScreenContainer.id = "full__slideshow__container";
            Object.assign(this.fullScreenContainer.style, {
                position: "fixed",
                top: "0", left: "0",
                width: "100%", height: "100%",
                background: "rgba(0,0,0,0.97)",
                zIndex: "99999",
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                flexDirection: "column"
            });
            document.body.appendChild(this.fullScreenContainer);
        }

        const sizes = { small: "60%", medium: "80%", large: "100%" };

        this.fullScreenContainer.innerHTML = `
        <div id="closeSlide" style="position:absolute;top:20px;right:30px;font-size:52px;color:#fff;
            cursor:pointer;width:70px;height:70px;line-height:70px;text-align:center;
            background:rgba(255,255,255,0.12);border-radius:50%;backdrop-filter:blur(12px);
            user-select:none;">
            ✕
        </div>
        <img src="${imageUrls[this.currentIndex]}" alt="Slideshow image"
             style="max-width:${sizes[this.settings.slideshowSize]};
                    max-height:${sizes[this.settings.slideshowSize]};
                    object-fit:contain;
                    border-radius:16px;
                    box-shadow:0 20px 40px rgba(0,0,0,0.7);
                    transition:opacity 0.4s ease, transform 0.3s ease;
                    opacity:0;">
    `;

        const imgEl = this.fullScreenContainer.querySelector("img");
        imgEl.onload = () => imgEl.style.opacity = "1";

        document.getElementById("closeSlide").onclick = () => {
            this.fullScreenContainer.style.display = "none";
        };

        const switchHandler = (e) => {
            e.preventDefault();
            if (e.type === "click" || e.deltaY > 0) {
                this.currentIndex = (this.currentIndex + 1) % imageUrls.length;
            } else {
                this.currentIndex = (this.currentIndex - 1 + imageUrls.length) % imageUrls.length;
            }
            imgEl.src = imageUrls[this.currentIndex];
        };
        this.fullScreenContainer.onclick = switchHandler;
        this.fullScreenContainer.onwheel = switchHandler;

        const keyHandler = (e) => {
            if (["ArrowRight", "ArrowDown", "d", " ", "Enter"].includes(e.key)) {
                switchHandler({ type: "click", preventDefault: () => {} });
            } else if (["ArrowLeft", "ArrowUp", "a"].includes(e.key)) {
                switchHandler({ type: "wheel", deltaY: -1, preventDefault: () => {} });
            } else if (e.key === "Escape") {
                this.fullScreenContainer.style.display = "none";
            }
        };
        document.addEventListener("keydown", keyHandler);

        this.fullScreenContainer.style.display = "flex";
    }

    async waitForElement(selector, parentElement = null, interval = 100) {
        return new Promise((resolve, reject) => {
            let intervalId;
            const checkElement = () => {
                const element = parentElement !== null ? parentElement.querySelector(selector) : document.querySelector(selector);
                if (element) {
                    clearInterval(intervalId);
                    resolve(element);
                }
            };
            checkElement();
            intervalId = setInterval(checkElement, interval);
        });
    }

    nextImage() {
        this.currentIndex = (this.currentIndex + 1) % this.images.length;
        this.updateSlide();
    }

    prevImage() {
        this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
        this.updateSlide();
    }

    updateSlide() {
        const img = this.fullScreenContainer.querySelector("img");
        img.src = this.images[this.currentIndex];
    }

    openSettingsPanel() {
        if (document.getElementById("imgmode__settings")) return;
        const panel = document.createElement("div");
        panel.id = "imgmode__settings";
        panel.innerHTML = `
                <div style="position:fixed;top:0;left:0;width:100%;height:100%;
                            background:rgba(0,0,0,0.5);z-index:9998;" onclick="this.parentNode.remove()"></div>
                <div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
                            background:#fff;color:#000;padding:24px;border-radius:16px;
                            box-shadow:0 20px 40px rgba(0,0,0,0.4);z-index:9999;min-width:320px;">
                    <h3 style="margin:0 0 20px;text-align:center;">圖片顯示設定</h3>
                    <label style="display:block;margin:15px 0;">
                        網格欄數:
                        <input type="range" min="1" max="12" value="${this.settings.gridColumns}" id="colRange">
                        <b id="colNum" style="margin-left:10px;">${this.settings.gridColumns}</b> 欄
                    </label>
                    <label style="display:block;margin:15px 0;">
                        幻燈片大小:
                        <select id="sizeSel" style="width:100%;padding:8px;margin-top:8px;">
                            <option value="small">小 (60%)</option>
                            <option value="medium">中 (80%)</option>
                            <option value="large">大 (100%)</option>
                        </select>
                    </label>
                    <label style="display:block;margin:20px 0;">
                        <input type="checkbox" id="autoSlide"${this.settings.autoSlideshow ? " checked" : ""}>
                        點選小圖直接進入幻燈片
                    </label>
                    <div style="text-align:center;">
                        <button id="saveSet" style="padding:10px 24px;background:#28a745;color:white;
                                                   border:none;border-radius:8px;font-size:16px;cursor:pointer;">
                            儲存並套用
                        </button>
                    </div>
                </div>
            `;
        document.body.appendChild(panel);
        document.getElementById("sizeSel").value = this.settings.slideshowSize;
        const range = document.getElementById("colRange");
        const num = document.getElementById("colNum");
        range.oninput = () => num.textContent = range.value;
        document.getElementById("saveSet").onclick = () => {
            this.settings.gridColumns = parseInt(range.value);
            this.settings.slideshowSize = document.getElementById("sizeSel").value;
            this.settings.autoSlideshow = document.getElementById("autoSlide").checked;
            GM_setValue("gridColumns", this.settings.gridColumns);
            GM_setValue("slideshowSize", this.settings.slideshowSize);
            GM_setValue("autoSlideshow", this.settings.autoSlideshow);
            //console.log(GM_getValues(["gridColumns","slideshowSize","autoSlideshow"]))
            panel.remove();
            this.tidyUpPostImage();
        };
    }

    observeDOM() {
        const observer = new MutationObserver(() => {
            const postFiles = document.querySelector("div.post__files");
            if (postFiles && !postFiles.dataset.gridProcessed) {
                this.tidyUpPostImage();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
}

new ImageGridEnhancer();
GM_addStyle(`
    .image-grid img:hover { transform:scale(1.06); }
    #full-slideshow-container img { transition: all 0.4s ease; }
    button:hover { filter: brightness(1.15); }
`);