Kemono/Coomer grid layout for post

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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); }
`);