18Comic 之路

JM / 18Comic 车牌号划词查询工具

// ==UserScript==
// @name         18Comic 之路
// @namespace    http://tampermonkey.net/
// @version      0.2
// @license      MIT
// @description  JM / 18Comic 车牌号划词查询工具
// @author       zyf722
// @match        *://weibo.com/*
// @match        *://*.weibo.com/*
// @match        *://*.weibo.cn/*
// @match        *://tieba.baidu.com/*
// @match        *://*.bilibili.com/
// @match        *://*.bilibili.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=18comic.vip
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // Site source selection
    var JM_SITE = GM_getValue("JM_SITE", "18comic.vip");
    var JM_CURRENT = GM_getValue("JM_CURRENT", 0);
    const updateSite = (site) => {
        JM_SITE = site;
        GM_setValue("JM_SITE", JM_SITE);
    }
    const updateCurrent = (current) => {
        JM_CURRENT = current;
        GM_setValue("JM_CURRENT", JM_CURRENT);
    }
    const sources = [
        "18comic.vip",
        "18comic.org",
        "jmcomic1.me",
        "18comic-palworld.vip",
        "18comic-c.art"
    ];
    const updateMenuCommandFactory = (index) => {
        return () => {
            updateSite(sources[index]);
            GM_registerMenuCommand("线路 " + (JM_CURRENT + 1) + ": " + sources[JM_CURRENT], updateMenuCommandFactory(JM_CURRENT), {id: JM_CURRENT});
            updateCurrent(index);
            GM_registerMenuCommand("✅ 线路 " + (index + 1) + ": " + sources[index], updateMenuCommandFactory(index), {id: index});
        };
    }
    for (var i = 0; i < sources.length; i++) {
        GM_registerMenuCommand((JM_CURRENT === i ? "✅ " : "") + "线路 " + (i+1) + ": " + sources[i], updateMenuCommandFactory(i), {id: i});
    };

    // Util functions
    const createElementWithAttr = (tag, attr) => {
        const element = document.createElement(tag);

        if (attr) {
            Object.entries(attr).forEach(([key, value]) => {
                element.setAttribute(key, value);
            });
        }
        return element;
    };

    const createSVGPath = (path) => {
        const p = document.createElementNS("http://www.w3.org/2000/svg", 'path');
        p.setAttribute('d', path);
        p.setAttribute('fill', 'currentColor');
        return p;
    };

    const createSVGElement = (path, viewBox, width, height, attr) => {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute('viewBox', viewBox);
        svg.setAttribute('width', width);
        svg.setAttribute('height', height);
        if (attr) {
            Object.entries(attr).forEach(([key, value]) => {
                svg.setAttribute(key, value);
            });
        }

        if (typeof path === 'string') {
            const p = createSVGPath(path);
            svg.appendChild(p);
            return [svg, p];
        } else if (Array.isArray(path)) {
            const paths = path.map(p => {
                const newPath = createSVGPath(p);
                svg.appendChild(newPath);
                return newPath;
            });
            return [svg, ...paths];
        } else {
            throw new Error('Invalid path type');
        }
    };

    const popupWindow = createElementWithAttr('div', {id: 'jm-popup', class: 'jm-select-none'});
    document.body.appendChild(popupWindow);

    const numberContainer = createElementWithAttr('div', {id: 'jm-number-container'});
    popupWindow.appendChild(numberContainer);

    const LOADING_ICON = "M512 170.666667a341.333333 341.333333 0 1 0 0 682.666666 341.333333 341.333333 0 0 0 0-682.666666zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z m426.666667-256a42.666667 42.666667 0 0 1 42.666667 42.666667v195.669333l115.498666 115.498667a42.666667 42.666667 0 0 1-60.330666 60.330666l-128-128A42.666667 42.666667 0 0 1 469.333333 512V298.666667a42.666667 42.666667 0 0 1 42.666667-42.666667z";
    const FAIL_ICON = "M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m129.29219 160.304762l51.736381 51.736381L563.687619 512l129.316571 129.29219-51.73638 51.736381L512 563.687619l-129.29219 129.316571-51.736381-51.73638L460.312381 512l-129.316571-129.26781 51.73638-51.73638L512 460.263619l129.26781-129.29219z";
    const SUCCESS_ICON = 'M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m193.194667 145.188571l52.467809 50.956191-310.662095 319.683047-156.379429-162.230857 52.662858-50.761143 103.936 107.812572 257.974857-265.45981z';
    // Define the warning icon SVG path
    const WARNING_ICON = "M545.718857 130.608762c11.337143 6.265905 20.699429 15.555048 26.989714 26.819048l345.014858 617.667047a68.87619 68.87619 0 0 1-26.989715 93.915429c-10.313143 5.705143-21.942857 8.704-33.718857 8.704H166.985143A69.266286 69.266286 0 0 1 97.52381 808.643048c0-11.751619 2.998857-23.28381 8.752761-33.548191l344.990477-617.642667a69.656381 69.656381 0 0 1 94.451809-26.819047zM512 191.000381L166.985143 808.643048H856.990476L512 191.000381zM546.718476 670.47619v69.071239h-69.461333V670.47619h69.485714z m0-298.374095v252.318476h-69.461333V372.102095h69.485714z";

    // Create an SVG element for the number icon
    const [numberIcon, numberIconPath] = createSVGElement(LOADING_ICON, '0 0 1024 1024', '16px', '16px', {id: 'jm-number-icon'});
    numberContainer.appendChild(numberIcon);

    // Create a div element for the number text
    const numberText = createElementWithAttr('div', {id: 'jm-number', class: 'jm-select-none jm-overflow'});
    numberContainer.appendChild(numberText);

    // Create an anchor element for the title text
    const titleText = createElementWithAttr('a', {id: 'jm-title-text', class: 'jm-select-none jm-overflow jm-title'});
    titleText.setAttribute('target', '_blank');
    titleText.setAttribute('rel', 'noopener noreferrer');
    popupWindow.appendChild(titleText);

    // Create a div element for the title loading text
    const titleLoadingText = createElementWithAttr('div', {id: 'jm-title-loading', class: 'jm-select-none jm-title'});
    titleLoadingText.innerHTML = '加载中...';
    popupWindow.appendChild(titleLoadingText);

    // Function to toggle the loading status
    const toggleLoading = (status) => {
        if (status === "loading") {
            titleLoadingText.style.display = 'inline';
            titleText.style.display = 'none';
            numberIconPath.setAttribute('d', LOADING_ICON);
            numberText.style.color = numberIcon.style.color = "black";
        } else if (status === "fail") {
            titleLoadingText.style.display = 'none';
            titleText.style.display = 'inline';
            numberIconPath.setAttribute('d', FAIL_ICON);
            numberText.style.color = numberIcon.style.color = "red";
        } else if (status === "done") {
            titleLoadingText.style.display = 'none';
            titleText.style.display = 'inline';
            numberIconPath.setAttribute('d', SUCCESS_ICON);
            numberText.style.color = numberIcon.style.color = "green";
        } else if (status === "warning") {
            titleLoadingText.style.display = 'none';
            titleText.style.display = 'inline';
            numberIconPath.setAttribute('d', WARNING_ICON);
            numberText.style.color = numberIcon.style.color = "orange";
        }
    };

    // Create a button element for the copy button
    const copyBtn = createElementWithAttr('button', {id: 'jm-copy'});
    popupWindow.appendChild(copyBtn);

    // Define the copy icon SVG path
    const DONE_ICON = "M512 16C238.066 16 16 238.066 16 512s222.066 496 496 496 496-222.066 496-496S785.934 16 512 16z m0 96c221.064 0 400 178.902 400 400 0 221.064-178.902 400-400 400-221.064 0-400-178.902-400-400 0-221.064 178.902-400 400-400m280.408 260.534l-45.072-45.436c-9.334-9.41-24.53-9.472-33.94-0.136L430.692 607.394l-119.584-120.554c-9.334-9.41-24.53-9.472-33.94-0.138l-45.438 45.072c-9.41 9.334-9.472 24.53-0.136 33.942l181.562 183.032c9.334 9.41 24.53 9.472 33.94 0.136l345.178-342.408c9.408-9.336 9.468-24.532 0.134-33.942z";
    const COPY_ICON = 'M931.882 131.882l-103.764-103.764A96 96 0 0 0 760.236 0H416c-53.02 0-96 42.98-96 96v96H160c-53.02 0-96 42.98-96 96v640c0 53.02 42.98 96 96 96h448c53.02 0 96-42.98 96-96v-96h160c53.02 0 96-42.98 96-96V199.764a96 96 0 0 0-28.118-67.882zM596 928H172a12 12 0 0 1-12-12V300a12 12 0 0 1 12-12h148v448c0 53.02 42.98 96 96 96h192v84a12 12 0 0 1-12 12z m256-192H428a12 12 0 0 1-12-12V108a12 12 0 0 1 12-12h212v176c0 26.51 21.49 48 48 48h176v404a12 12 0 0 1-12 12z m12-512h-128V96h19.264c3.182 0 6.234 1.264 8.486 3.514l96.736 96.736a12 12 0 0 1 3.514 8.486V224z';

    // Create an SVG element for the copy button icon
    const [copyBtnIcon, copyBtnCopyPath, copyBtnDonePath] = createSVGElement([COPY_ICON, DONE_ICON], '0 0 1024 1024', '16px', '16px', {id: 'jm-copy-icon'});
    copyBtnDonePath.classList.toggle('jm-copy-icon-hide');
    copyBtnCopyPath.classList.add('jm-copy-icon');
    copyBtnDonePath.classList.add('jm-copy-icon');
    copyBtn.appendChild(copyBtnIcon);

    // Function to disable the copy button
    const disableBtn = (status) => {
        copyBtn.disabled = status;
        copyBtn.style.pointerEvents = status ? 'none' : 'auto';
        copyBtnIcon.setAttribute('color', status ? 'gray' : 'dodgerblue');
    };
    disableBtn(true);

    // Create a style element for the CSS styles
    const style = createElementWithAttr('style');
    style.innerHTML = `
        .jm-select-none {
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            -khtml-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        .jm-overflow {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        #jm-popup {
            position: absolute;
            background-color: #fff;
            padding: 10px;
            margin-top: 10px;
            border: 1px solid #ddd;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 999999999999;
            display: none;
            max-width: 25%;
            column-gap: 10px;
            align-items: center;
        }

        .jm-title {
            max-width: 100%;
            font-size: 14px;
            grid-column: 1;
            grid-row: 2;
        }

        #jm-title-text {
            display: none;
        }

        #jm-number-container {
            max-width: 100%;
            grid-column: 1;
            grid-row: 1;
            display: flex;
            align-items: center;
        }

        #jm-number {
            font-size: 18px;
            font-weight: bold;
        }

        #jm-number-icon {
            margin-right: 5px;
        }

        #jm-copy {
            border: none;
            background-color: #fff;
            width: 32px;
            height: 32px;
            font-size: 16px;
            cursor: pointer;
            grid-column: 2;
            grid-row: 1 / 3;
            transition: background-color 0.3s;
        }

        #jm-copy:hover:not(:disabled) {
            background-color: #f6f6f6;
        }

        #jm-copy:active:not(:disabled) {
            background-color: #e6e6e6;
        }

        .jm-copy-icon {
            transition: opacity 0.25s;
        }

        .jm-copy-icon-hide {
            opacity: 0;
        }
    `;
    document.head.appendChild(style);

    // Function to fetch the title of a URL
    const fetchTitle = (url, callback) => {
        console.log("fetching " + url);
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function(response) {
                const title = response.responseText.match(/<title[^>]*>([^<]+)<\/title>/)[1];
                callback(title.replace(" Comics - 禁漫天堂", ""));
            }
        });
    };

    // Function to copy the title text to the clipboard
    const copyToClipboard = (event) => {
        navigator.clipboard.writeText(titleText.innerText);
        copyBtn.style.pointerEvents = 'none';
        copyBtnCopyPath.classList.toggle('jm-copy-icon-hide');
        setTimeout(() => {
            copyBtnDonePath.classList.toggle('jm-copy-icon-hide');
        }, 250);
        setTimeout(() => {
            copyBtnDonePath.classList.toggle('jm-copy-icon-hide');
            setTimeout(() => {
                copyBtnCopyPath.classList.toggle('jm-copy-icon-hide');
                copyBtn.style.pointerEvents = 'auto';
            }, 250);
        }, 1500);
    };
    copyBtn.addEventListener('click', copyToClipboard);

    // Function to show the popup window
    const showPopup = (event) => {
        const selectedText = window.getSelection();

        // Check if mouse is inside the popup window
        if (!event.target.closest('#jm-popup')) {
            popupWindow.style.display = 'none';
            disableBtn(true);
        }

        if(selectedText.toString().trim() !== '') {
            const number = selectedText.toString().replace(/\D/g, '');

            if (popupWindow.style.display !== 'grid' && number !== "") {
                const range = selectedText.getRangeAt(0);
                const rect = range.getBoundingClientRect();
                const url = "https://" + JM_SITE + "/album/" + number;

                const activeEl = document.activeElement;
                if(['TEXTAREA','INPUT'].includes(activeEl.tagName)) rect = activeEl.getBoundingClientRect();

                const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

                const top = Math.floor(scrollTop + rect.top + rect.height);
                const left = Math.floor(rect.left);

                if(top === 0 && left === 0){
                    return;
                }

                popupWindow.style.left = left + 'px';
                popupWindow.style.top = top + 'px';

                numberText.innerHTML = number;
                numberText.style.color = "";

                toggleLoading("loading");

                popupWindow.style.display = 'grid';

                // Special optimization for nbnhhsh
                const nbnhhsh = document.getElementsByClassName("nbnhhsh-box nbnhhsh-box-pop")[0];
                if (nbnhhsh) nbnhhsh.style.top = (parseInt(nbnhhsh.style.top) + 80) + "px";

                fetchTitle(url, (title) => {
                    titleText.href = url;
                    if (title === "Just a moment...") {
                        titleText.innerHTML = titleText.title = "自动获取失败,请手动点击链接";
                        toggleLoading("warning");
                    } else if (title === "禁漫天堂") {
                        titleText.innerHTML = titleText.title = "无效车牌"
                        toggleLoading("fail");
                    } else {
                        titleText.innerHTML = titleText.title = title
                        toggleLoading("done");
                        disableBtn(false);
                    }
                });
            }
        }
    }

    // Function to show the popup window after a delay
    const _showPopup = (event) => {
        // Delay window.getSelection() to get the correct selected text
        setTimeout(() => {
            showPopup(event);
        }, 1);
    }

    // Add event listeners to show the popup window
    document.addEventListener('mouseup', _showPopup);
    document.addEventListener('keyup', _showPopup);
})();