CC98 Tools - Image Collections - GM

为CC98网页版添加收藏图片功能

// ==UserScript==
// @name         CC98 Tools - Image Collections - GM
// @version      1.0.3
// @description  为CC98网页版添加收藏图片功能
// @icon         https://www.cc98.org/static/98icon.ico

// @author       ml98
// @namespace    https://www.cc98.org/user/name/ml98
// @license      MIT

// @match        https://www.cc98.org/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-idle
// ==/UserScript==

(function () {
    "use strict";
    // Store
    const db = GMStore();

    // GM Store
    function GMStore() {
        async function add(image) {
            const images = GM_getValue('images', []);
            images.push(image);
            GM_setValue('images', images);
        }
        async function get(tags) {
            const images = GM_getValue('images', []);
            return images.filter(image => tags.some(tag => image.tags.includes(tag)));
        }
        async function del(urls) {
            const images = GM_getValue('images', []);
            const new_images = images.filter(image => !urls.includes(image.url));
            GM_setValue('images', new_images);
        }
        return { add, get, del };
    }

    // Components
    const imagePicker = ImagePicker({
        onSearch: async function (text) {
            const images = await db.get(text.split(" "));
            const result = images.map((image) => ({
                src: image.url,
                text: image.tags
                .filter((tag) => tag !== "default_tag")
                .join(" "),
            }));
            console.log("result", result);
            return result;
        },
        onDelete: async function (urls) {
            console.log("delete", urls);
            await db.del(urls);
        },
        onOK: async function (urls) {
            console.log("ok", urls);
            putText(urls.map((url) => `[img]${url}[/img]\n`).join(""));
        },
    });
    document.body.appendChild(imagePicker);

    const tagsInput = TagsInput({
        onSubmit: async function (text) {
            const tags = ["default_tag", ...text.split(" ").filter(Boolean)];
            console.log("save", tagsInput.imgSrc, "with tags", tags);
            await db.add({ url: tagsInput.imgSrc, tags: tags });
        },
    });
    document.body.appendChild(tagsInput);

    function putText(text) {
        const textarea = document.querySelector(".ubb-editor > textarea");
        if (!textarea) return;
        const setter = Object.getOwnPropertyDescriptor(
            window.HTMLTextAreaElement.prototype,
            "value"
        ).set;
        setter.call(textarea, textarea.value + text);
        textarea.dispatchEvent(new Event("input", { bubbles: true }));
    }

    function Modal() {
        const modal = element(`<div tabindex="0"><div class="ant-modal-mask"></div><div class="ant-modal-wrap"><div class="ant-modal" style="width:60%;"><div class="ant-modal-content">Modal</div></div></div></div>`);
        modal.show = () => { modal.style.display = "block"; modal.focus({ preventScroll: true }); };
        modal.hide = () => { modal.style.display = "none"; };
        on(modal.querySelector(".ant-modal-wrap"), "click", function (e) {
            e.target === this && modal.hide();
        });
        on(document.body, "keyup", function (e) {
            e.keyCode === 27 && modal.hide();
        });
        return modal;
    }

    function Input(i) {
        const input = element(`<span class="ant-input-group ant-input-group-compact" style="display:flex;"><input type="text" class="ant-input"/><button type="button" class="ant-btn ant-btn-primary" style="box-sizing:border-box;"><span>Submit</span></button></span>`);
        const $ = (s) => input.querySelector(s);
        const inputElement = $("input");
        inputElement.placeholder = i.placeholder || "input text";
        on($("button"), "click", async () => await i.onSubmit(inputElement.value));
        on(inputElement, "keyup", async function (e) {
            e.keyCode === 13 && (await i.onSubmit(inputElement.value));
        });
        return input;
    }

    function Item(i) {
        const item = element(`<div class="search-result-item"><img src="${i.src}" loading="lazy"/><p>${i.text}</p></div>`);
        item.select = () => item.classList.add("selected");
        on(item, "click", () => item.classList.toggle("selected"));
        return item;
    }

    function ImagePicker(i) {
        const modal = Modal();
        const $ = (s) => modal.querySelector(s);
        const $$ = (s) => [...modal.querySelectorAll(s)];
        $(".ant-modal-content").innerHTML = `<button class="ant-modal-close"><span class="ant-modal-close-x"></span></button><div class="ant-modal-header"><div class="ant-modal-title">Search</div></div><div class="ant-modal-body"><div class="ant-list" tabindex="0" style="height:20rem;margin-top:1em;overflow-y:auto;"></div></div><div class="ant-modal-footer"><div><button type="button" class="ant-btn ant-btn-danger"><span>删 除</span></button><button type="button" class="ant-btn ant-btn-primary"><span>确 定</span></button></div></div>`;
        on($(".ant-modal-close"), "click", () => modal.hide());
        on($(".ant-btn-danger"), "click", async function () {
            await i.onDelete(
                $$(".search-result-item.selected>img").map((img) => img.src)
            );
        });
        on($(".ant-btn-primary"), "click", async function () {
            await i.onOK(
                $$(".search-result-item.selected>img").map((img) => img.src)
            );
            modal.hide();
        });
        const list = $(".ant-list");
        on(list, "keydown", function (e) {
            if (e.ctrlKey && e.code === "KeyA") {
                e.preventDefault();
                $$(".search-result-item").forEach((item) => item.select());
            }
        });
        const search = Input({
            placeholder: "Search by tags (default_tag)",
            onSubmit: async (text) => {
                const result = await i.onSearch(text);
                list.innerHTML = "";
                list.append(...result.map((item) => Item(item)));
            },
        });
        const body = $(".ant-modal-body");
        body.insertBefore(search, body.firstChild);
        modal.hide();
        return modal;
    }

    function TagsInput(i) {
        const modal = Modal();
        const $ = (s) => modal.querySelector(s);
        $(".ant-modal-content").innerHTML = `<div class="ant-modal-body"></div>`;
        const input = Input({
            placeholder: "Enter tags, separated by spaces",
            onSubmit: async (text) => {
                await i.onSubmit(text);
                modal.hide();
            },
        });
        const body = $(".ant-modal-body");
        body.insertBefore(input, body.firstChild);
        modal.hide();
        return modal;
    }

    GM_addStyle(`
      .search-result-item { border-radius:4px; display:inline-block; margin:4px; outline:solid 1px lightgray; padding:2px; }
      .search-result-item.selected { outline:solid 2px deepskyblue; }
      .search-result-item>img { border-radius:4px; max-height:150px; overflow:hidden; }
    `);

    // Observer to add or remove button
    Observe(document.body, callback);

    function Observe(targetNode, callback, config) {
        config = config || {
            attributes: false,
            childList: true,
            subtree: true,
        };
        const observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
        return observer;
    }

    function callback(mutationsList) {
        for (const mutation of mutationsList) {
            if (mutation.type === "childList") {
                for (const node of mutation.addedNodes) {
                    if (node.classList?.contains("ubb-image-toolbox")) {
                        addSaveButton(node);
                    } else if (
                        node.classList?.contains("ubb-editor") ||
                        node.classList?.contains("fa-smile-o") ||
                        node.id === "sendTopicInfo"
                    ) {
                        addImagePickerButton();
                    }
                }
                for (const node of mutation.removedNodes) {
                    if (node.classList?.contains("fa-smile-o")) {
                        removeImagePickerButton();
                    }
                }
            }
        }
    }

    function addSaveButton(toolbox) {
        // console.log('addSaveButton');
        const saveButton = element(
            `<button><i class="fa fa-bookmark"></i></button>`
        );
        on(saveButton, "click", () => {
            tagsInput.imgSrc = toolbox.nextSibling.src;
            tagsInput.show();
        });
        toolbox.insertBefore(saveButton, toolbox.firstChild);
    }

    function addImagePickerButton() {
        const referenceNode = document.querySelector(".fa-smile-o.ubb-button");
        if (!referenceNode) return;
        // console.log('addImagePickerButton');
        const imagePickerButton = element(
            `<button type="button" class="fa fa-bookmark ubb-button" title="收藏"></button>`
        );
        on(imagePickerButton, "click", () => {
            imagePicker.show();
        });
        referenceNode.parentNode.insertBefore(
            imagePickerButton,
            referenceNode.nextSibling
        );
    }

    function removeImagePickerButton() {
        const imagePickerButton = document.querySelector(
            ".fa-bookmark.ubb-button"
        );
        if (!imagePickerButton) return;
        // console.log('removeImagePickerButton');
        imagePickerButton.remove();
    }

    function on(elem, event, func) {
        return elem.addEventListener(event, func, false);
    }

    function element(html) {
        var t = document.createElement("template");
        t.innerHTML = html.trim();
        return t.content.firstChild;
    }
})();