搜索那个

日本影视行业内部搜索工具Pro Max Plus Ultra

// ==UserScript==
// @name         搜索那个
// @description  日本影视行业内部搜索工具Pro Max Plus Ultra
// @author       shopkeeperV
// @namespace    https://greasyfork.org/zh-CN/users/150069
// @version      1.3.6
// @match        *://*/*
// @connect *
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// ==/UserScript==
/*jshint esversion: 8*/
(async function () {
    'use strict';
    if (top !== window) {
        //仅在iframe内执行,服务于分辨率获取功能,负责iframe内视频信息反馈
        //注意广告插件或许会导致获取不到视频信息
        if (!/(cloudflare)|(captcha)/.test(window.location.href)) {
            //iframe获取父窗口消息,绕过iframe安全机制
            window.addEventListener('message', e => {
                //需要判断一下内容,部分网站自己也会通信,会触发到这个方法,JavSearch作为标志
                if (typeof e.data === 'string' && e.data.includes("JavSearch")) {
                    //发送页面视频信息
                    window.top.postMessage(getVideosInfo(), '*');
                }
            });
        }
        //去部分iframe弹窗
        let host = location.host;
        if (/emturbovid/.test(host)) {
            addCSS("div[id^='pop']{display:none;}");
        } else if (/fc2stream/.test(host)) {
            addCSS("div[style*='opacity:0']{display:none;}");
        }
        //7mm系列弹窗
        else if (/mm.*xyz/.test(host)) {
            addCSS("div[class*='pop']{display:none;}");
            addCSS("div[style*='opacity:0']{display:none;}");
        }
        //为此iframe添加message事件后无需往后执行
        return;
    }
    let isAndroid = /android/i.test(navigator.userAgent);
    //设置样式,通知模态框要用
    initStyle();
    //通知模态框需要率先创建
    let noticeDialogObj = new ModalDialog("<div id='jav_notice'></div>", {dialogPosition: "top"});
    //执行个别网页的定制脚本
    if (await adviceAndReturn()) return;
    //最近一次的搜索词
    let historyKeyword = "";
    //待分词文本
    let textContent = "";
    //分辨获取功能的页面视频信息描述
    let videosInfo = "";
    //分词时特殊片商识别
    let specialProducer = "";
    //用于判断打开小球时是否使用历史搜索词
    let isNewLinkWord = false;
    //桌面端链接分词定时器
    let timeoutTimer;
    //功能是否初始化
    let ready = false;
    //灵魂搜索引擎们
    let btnMap;
    let cenMap;
    let uncMap;
    let sampleMap;
    let searchDialogPosition, blingBlurRadius, blingSpreadRadius;
    if (isAndroid) {
        searchDialogPosition = "bottom";
        blingBlurRadius = "50px";
        blingSpreadRadius = "30px";
    } else {
        searchDialogPosition = "middle";
        blingBlurRadius = "500px";
        blingSpreadRadius = "60px";
    }
    buildBtnMap();
    let searchDialogObj = new ModalDialog(searchDialogCode(), {
        width: "310px",
        dialogPosition: searchDialogPosition
    });
    let settingDialogObj;
    let chooseDialogObj;
    //大量使用,单独声明,注意需要模态框创建后才有,写在后面
    let textInput = document.getElementById("jav_input");
    //创建悬浮球
    new FloatBall("jav_ball", {color: "#00cec9"});
    //让屏幕周围亮起来,用于链接分词
    let blingObj = new WindowBling({
        color: "#00cec9",
        blingPosition: ["left", "right"],
        blurRadius: blingBlurRadius,
        spreadRadius: blingSpreadRadius
    });
    //仅在初始化设置模态框时使用一次,其他都从存储中获取
    let mosaic_reduce_first = getTmValue("mosaic_reduce_first");
    if (mosaic_reduce_first == null) {
        setTmValue("mosaic_reduce_first", false);
    }
    //第一阶段初始化,绑定必须的监听事件,小球,链接分词,划词等,其余的在首次点击小球后再初始化,以节约性能
    init1();

    function buildBtnMap() {
        btnMap = new Map();
        btnMap.set("avmoo有码情报", "https://avmoo.online/cn/search/%s");
        btnMap.set("avsox无码情报", "https://avsox.click/cn/search/%s");
        btnMap.set("javdb情报", "https://javdb.com/search?q=%s&f=all");
        btnMap.set("google+jav", "https://www.google.com/search?q=%s%20jav");
    }

    function buildCenMap() {
        cenMap = new Map();
        let part1 = function () {
            cenMap.set("supjav破解", {url: "https://supjav.com/zh/?s=%s", reqMethod: "get"});
        }
        let part2 = function () {
            cenMap.set("missav字幕", {url: "https://missav.com/cn/search/%s", reqMethod: "get"});
            cenMap.set("jable字幕", {url: "https://jable.tv/search/%s/", reqMethod: "get"});
            cenMap.set("7mm字幕", {
                url: "https://7mmtv.sx/zh/searchform_search/all/index.html",
                reqMethod: "post",
                payload: "search_keyword=%s&search_type=searchall&op=search"
            });
            cenMap.set("ggjav字幕", {url: "https://ggjav.com/main/search?string=%s", reqMethod: "get"});
            cenMap.set("777字幕", {url: "https://www.jav777.xyz/?s=%s", reqMethod: "get"});
        }
        if (getTmValue("mosaic_reduce_first")) {
            part1();
            part2();
        } else {
            part2();
            part1();
        }
        //搜索到分割线时,标志着破解版本和字幕版本不存在,打开之前保留的普通版本
        cenMap.set("---", null);
        cenMap.set("today", {url: "https://javhd.today/search/video/?s=%s", reqMethod: "get"});
    }

    function buildUncMap() {
        uncMap = new Map();
        uncMap.set("missav无码", {url: "https://missav.com/cn/search/%s", reqMethod: "get"});
        uncMap.set("guru", {url: "https://jav.guru/?s=%s", reqMethod: "get"});
        uncMap.set("7mm无码", {
            url: "https://7mmtv.sx/zh/searchform_search/all/index.html",
            reqMethod: "post",
            payload: "search_keyword=%s&search_type=uncensored&op=search"
        });
        uncMap.set("njav", {url: "https://njav.tv/zh/search?keyword=%s", reqMethod: "get"});
        uncMap.set("supjav无码", {url: "https://supjav.com/zh/?s=%s", reqMethod: "get"});
    }

    //为常规格式的(aaa-111)视频预览引擎也添加设置选项,防止他们抽风,其他格式的引擎不需要是因为他们都是独一无二的,抽风也没得换
    function buildSampleMap() {
        sampleMap = new Map();
        sampleMap.set("trailer", {url: "https://javtrailers.com/search/%s"});
        sampleMap.set("jav24", {url: "https://www.jav24.com/?q=%s"});
        sampleMap.set("javLand", {url: "https://jav.land/tw/id_search.php?keys=%s"});
        sampleMap.set("javdb", {url: "https://javdb.com/search?q=%s&f=all"});
    }

    function searchDialogCode() {
        return '<div id="jav_searchDialog">' +
            '<div>' + getBtns() +
            '<div id="jav_cen" class="bbtn llink">搜索有码</div>' +
            '<div id="jav_unc" class="bbtn llink">搜索无码</div>' +
            '</div>' +
            '<div><input id="jav_input" type="text" value="" autocomplete="off"/></div>' +
            '<div>' +
            '<div id="jav_copy" class="bbtn ttool">复制到剪贴板</div>' +
            '<div id="jav_clear" class="bbtn ttool">清空输入框</div>' +
            '<div id="jav_analyze" class="bbtn ttool">析出番号</div>' +
            '<div id="jav_sample" class="bbtn ttool">视频预览</div>' +
            '<div id="jav_video_size" class="bbtn ttool">分辨率获取</div>' +
            '<div id="jav_setting" class="bbtn ttool">搜索设置</div>' +
            '<div id="jav_description" class="bbtn ttool">必看说明</div>' +
            '</div>' +
            '<div><div id="jav_close" class="bbtn cclose">关闭</div></div>' +
            '</div>';

        function getBtns() {
            let btns = "";
            btnMap.forEach(function (value, key) {
                btns += `<div class='bbtn llink' id='${key}' jav_url='${value}'>${key}</div>`;
            });
            return btns;
        }
    }

    function settingCode() {
        return "<div id='jav_settingDialog'>" +
            "<div class='jav_setting_text'>下列按钮设置效果永久保留。</div>" +
            "<div id='mosaic_reduce_first' class='bbtn llink sset'>" +
            "当前是:" + (mosaic_reduce_first ? "无码破解" : "中文字幕") + "优先</div>" +
            "<div class='jav_setting_text' >下列开关决定相应引擎是否参与搜索,在搜索结果不理想时使用,仅在当前页面生效。</div>" +
            "<div>" +
            "<span class='jav_setting_text'>有码引擎</span><br/>" +
            "<form class='jav_pick_form' id='cen_pick_form'>" +
            getSettingBtns(cenMap) +
            "</form>" +
            "</div>" +
            "<div>" +
            "<span class='jav_setting_text'>无码引擎</span><br/>" +
            "<form class='jav_pick_form' id='unc_pick_form'>" +
            getSettingBtns(uncMap) +
            "</form>" +
            "</div>" +
            "<div>" +
            "<form class='jav_pick_form' id='sample_pick_form'>" +
            "<b>预览引擎</b><br/>" +
            getSettingBtns(sampleMap) +
            "</form>" +
            "</div>" +
            "</div>";
    }

    function getSettingBtns(map) {
        let result = "";
        map.forEach((value, key) => {
            if (/---/.test(key)) {
                result += `<input type='checkbox' checked id='${key}'><label>${key}</label><div class='jav_go'>分隔</div><br/>`;
            } else {
                let urlPart = value.url.split("/");
                result += `<input type='checkbox' checked id='${key}'><label for='${key}'>${key}</label>` +
                    `<div class='jav_go' jav_url='${urlPart[0]}//${urlPart[2]}'>前往</div>` +
                    "<br/>";
            }

        });
        return result;
    }

    function initStyle() {
        let globalCSS = ".ddialog .bbtn:focus,.ddialog .bbtn:active,.ddialog .bbtn:hover{-webkit-tap-highlight-color:transparent}" +
            ".ddialog .bbtn{box-sizing:border-box;display:inline-block;margin:3px;padding:0 8px;border-radius:4px;cursor:pointer;color:white;font:16px/1.8 sans-serif;min-width:140px;min-height:30px;letter-spacing:normal;user-select:none}" +
            ".ddialog .llink{background:#00baf8;}" +
            ".ddialog .ttool{background:#00baf8;}" +
            ".ddialog .cclose{background:#ff6666;}" +
            "#jav_input{box-sizing:border-box;display:block;margin:3px auto;outline:none;border:3px solid #00baf8 !important;border-radius:6px;padding:6px;width:286px;height:35px;font:14px/1.8 sans-serif !important;text-align:center;color:#00baf8 !important;font-weight:bold !important;background-color:white !important;box-shadow:none;letter-spacing:normal;}" +
            "#jav_input::placeholder{font:14px/1.8 sans-serif !important;color:#00baf8 !important;text-align:center;letter-spacing:normal;}" +
            "#mmuted{position:fixed;box-sizing:border-box;z-index:999999;border-radius:4px;cursor:pointer;user-select: none;margin:auto;color:white;padding:15px 20px;text-align:center;font:18px/1.8 sans-serif;letter-spacing:normal;}" +
            "#jav_notice,#jav_choose{font:18px/1.8 sans-serif;text-align:center;color:#00baf8;font-weight:bold;padding:10px;letter-spacing:normal;}" +
            "#jav_notice .eerror{color:#ff6666;}" +
            ".jav_pick_form{text-align:center !important;}" +
            ".jav_pick_form input{display:none !important;}" +
            ".jav_pick_form label,.jav_go{border:1px solid #00baf8;padding:2px 5px 2px 5px;text-align:center;margin:5px;border-radius:5px;color:#00baf8;cursor:pointer;display:inline-block !important;font:14px/1.6 sans-serif;-webkit-tap-highlight-color:transparent;user-select:none;}" +
            ".jav_pick_form label{min-width:110px;}" +
            ".jav_pick_form input:checked + label{color:white;background:#00baf8;}" +
            "#jav_searchDialog{margin-top:15px;margin-bottom:10px;}" +
            "#jav_settingDialog{width:200px;margin:20px auto;font:14px/1.6 sans-serif;color:#00baf8;}" +
            "#jav_settingDialog div:not(.jav_go){margin-bottom:10px;}" +
            "#jav_settingDialog .sset{font:14px/2.3 sans-serif;}" +
            ".jav_setting_text{font-weight:bold;}";
        addCSS(globalCSS);
        //下面是特定网页的样式
        let host = location.host;
        let path = location.pathname;
        if (/fc2.com/.test(host) && /article/.test(path)) {
            addCSS('#fc2Div>div{background-color:#00baf8;border-radius:4px;color:white;padding:5px 8px;user-select:none;' +
                'cursor: pointer;margin:5px 10px;text-align:center;display:inline-block;font:12px/1.6 sans-serif;}');
        } else if (/7mmtv/.test(host)) {
            //广告图,css隐藏比js快
            addCSS(".ut_cg1_top{display:none;}");
            //去视频悬浮层
            addCSS(".mvspan_2_s_k_i_p_row{display:none;}");
            addCSS("div[class*='pop']{display:none;}");
        } else if (/jav777/.test(host)) {
            addCSS("body{min-width:unset !important;}");
        } else if (/javdb/.test(host)) {
            addCSS("body{margin-right:0px !important;}");
        } else if (/supjav/.test(host)) {
            addCSS(".movv-ad{display:none;}");
        }
    }

    function addCSS(css) {
        let style = document.createElement("style");
        //放body里是因为部分网页重写head标签
        document.body.prepend(style);
        style.textContent = css;
    }

    //此方法可以完成一些网站的自动点击、自动播放等,也对一些网站进行功能定制
    async function adviceAndReturn() {
        //需要强制播放的窗口,有时是iframe所以需要改变的
        let videoWindow = window;
        let voiceDiv;
        let failTimes = 0;
        let host = location.host;
        let path = location.pathname;
        let autoplay = getTmValue("autoplay");
        setTmValue("autoplay", false);
        let show_img = getTmValue("show_img");
        setTmValue("show_img", false);
        //7mm自定义播放源优先级
        if (/7mmtv/.test(host)) {
            //视频观看页面
            if (document.getElementsByClassName("fullvideo-details").length === 1) {
                //自动加载顺序:自定义数组>第一个源
                let btns = document.getElementsByClassName("btn-server");
                let sources = ["FL", "SW"];
                if (btns.length > 0) {
                    let openFirst = true;
                    out:for (let source of sources) {
                        for (let btn of btns) {
                            let func = btn.onclick.toString();
                            if (func.includes(source)) {
                                autoClick(btn);
                                openFirst = false;
                                break out;
                            }
                        }
                    }
                    if (openFirst) autoClick(btns[0]);
                }
            }
        }
        //fc2,添加两个易用的按钮
        else if (/fc2.com/.test(host) && /article/.test(path)) {
            //该元素后添加按钮
            let targetEle;
            //用户主页
            let home;
            if (isAndroid) {
                targetEle = document.getElementsByClassName("items_article_Mainitem")[0];
                home = document.getElementsByClassName("items_article_seller")[0].href;
            } else {
                targetEle = document.getElementsByTagName("ul")[3];
                home = targetEle.getElementsByTagName("a")[1].href;
            }
            let div = document.createElement("div");
            div.id = "fc2Div";
            div.innerHTML = "<div>该作者其他作品</div><div>搜索该作品</div>";
            targetEle.after(div);
            div.children[0].addEventListener("click", () => {
                window.location.href = home + "articles?sort=date&order=desc";
            });
            div.children[1].addEventListener("click", () => {
                textInput.value = window.location.href.match(/\/([0-9]+)\//)[1];
                historyKeyword = textInput.value;
                if (!ready) {
                    init2();
                    ready = true;
                }
                searchDialogObj.open();
            });
        }
        //heyzo没有预览视频时将小图片替换高清图,无码片商中仅heyzo可以替换,因为他图片的url格式是固定的
        else if (/heyzo\.com/.test(host)) {
            //1秒后检测有没有视频元素,没有就展示预览大图
            setTimeout(() => {
                //没有视频才显示图片
                if (document.getElementsByTagName("video").length === 0) {
                    //查找所有图片,替换url
                    let gallery = document.getElementById("section_gallery");
                    if (gallery) {
                        let reg = /\/contents\/[0-9]+\/[0-9]+\/gallery\/thumbnail.*?\.jpg/ig;
                        let template = '<img src="%s" style="width:inherit;">';
                        let imgs = gallery.innerHTML.match(reg);
                        //往.quality-btns后添加图片
                        let html = "";
                        for (let i = 0; i < imgs.length; i++) {
                            html += template.replace("%s", imgs[i].replace("thumbnail_", ""));
                        }
                        let div = document.createElement("div");
                        div.style.width = "inherit";
                        div.innerHTML = html;
                        document.getElementById("quality-btns").after(div);
                    }
                }
            }, 1000);
        }

        if (show_img) {
            if (/trailer/.test(host)) {
                addCSS(".videoWrapper{display:none;}");
                let active = function () {
                    setTimeout(() => {
                        let gallery = document.getElementById("gallery-wrapper");
                        if (gallery) {
                            document.documentElement.scrollTop = gallery.offsetTop;
                            return;
                        } else active();
                        let picBtn = document.getElementById("description").getElementsByTagName("button")[0];
                        picBtn && autoClick(picBtn);
                    }, 500);
                }
                active();
            } else if (/javdb/.test(host)) {
                let image0 = document.getElementsByClassName("preview-images")[0];
                if (image0) {
                    document.documentElement.scrollTop = image0.offsetTop;
                }
            }
        } else if (autoplay) {
            //TokyoHot,只有移动端需要自动点击
            if (/tokyo-hot/.test(host) && isAndroid) {
                let playDiv = document.getElementsByClassName("fp-ui")[0];
                console.log("尝试点击");
                //不一定有视频
                playDiv && autoClick(playDiv);
                console.log("点击成功");
            } else if (/jav\.land/.test(host)) {
                let sample = document.getElementById("play_sample");
                sample && autoClick(sample);
            } else if (/trailer/.test(host)) {
                let loop = 0;
                //只能按钮播放还得等按钮加载的网站
                let handler = () => {
                    let btn = document.getElementsByClassName("vjs-big-play-button")[0];
                    let video = document.getElementsByTagName("video")[0];
                    loop++;
                    console.log("点击播放按钮。")
                    btn && autoClick(btn);
                    if (video) {
                        video.muted = true;
                        if (video.readyState === 0 && loop < 20) {
                            console.log("视频存在但未就绪。")
                            setTimeout(handler, 500);
                        }
                    } else {
                        console.log("视频不存在。")
                        setTimeout(handler, 500);
                    }

                };
                handler();
            } else if (/javdb/.test(host)) {
                let sample = document.getElementsByClassName("preview-video-container")[0];
                sample && autoClick(sample);
            } else if (/mgstage\.com/.test(host)) {
                let btn;
                if (isAndroid) {
                    btn = document.getElementById("sample-play");
                } else {
                    btn = document.getElementsByClassName("button_sample")[0];
                }
                btn && autoClick(btn);
            }
            voiceDiv = document.createElement("div");
            //给个通知,明确自动播放程序运行中
            voiceDiv.textContent = "等待视频加载...";
            voiceDiv.id = "mmuted";
            let _width = 180;
            voiceDiv.style.backgroundColor = "#ff6666";
            voiceDiv.style.width = _width + "px";
            voiceDiv.style.top = Math.round(screen.height / 3 * 2) + "px";
            voiceDiv.style.left = Math.round(screen.width / 2 - _width / 2) + "px";
            document.body.prepend(voiceDiv);
            //部分网站通过js加载视频或需要点击,所以会出现延迟,需要不停尝试获取页面的视频播放
            await forcePlay();
        }
        //默认情况不终止脚本执行
        return false;

        async function forcePlay() {
            if (failTimes === 30) {
                //15秒了还没播放
                console.log("自动播放失败,视频不存在或视频异常。");
                voiceDiv.innerHTML = "播放失败<br/>请尝试日本节点";
                voiceDiv.addEventListener("click", () => {
                    voiceDiv.style.display = "none";
                }, {once: true});
                return;
            }
            console.log("自动播放:尝试强制播放,每0.5s一次。");
            if (!videoWindow) return;
            let video = videoWindow.document.getElementsByTagName("video")[0];
            if (!video) {
                failTimes++;
                setTimeout(forcePlay, 500);
                return;
            }
            if (video.preload === "none") {
                video.preload = "metadata";
            }
            //个别网站display不写在内联样式里,需要以这种方式获取所有的样式getComputedStyle
            if (window.getComputedStyle(video).display === "none" || video.readyState === 0) {
                failTimes++;
                setTimeout(forcePlay, 500);
                return;
            }
            console.log("自动播放:获取到视频元素,清除自动播放循环定时器。");
            video.muted = true;
            console.log("自动播放:设为无声。");
            await video.play();
            voiceDiv.style.backgroundColor = "#00cec9";
            voiceDiv.textContent = "点我解除静音";
            voiceDiv.addEventListener("click", () => {
                video.muted = false;
                voiceDiv.textContent = "点我3倍速播放";
                voiceDiv.addEventListener("click", () => {
                    if (video.playbackRate === 1.0) {
                        video.playbackRate = 3.0;
                        voiceDiv.textContent = "点我恢复速度";
                    } else {
                        video.playbackRate = 1.0;
                        voiceDiv.textContent = "点我3倍速播放";
                    }
                });
            }, {once: true});
            console.log("自动播放:强制播放成功。");
        }

        function autoClick(eventTarget) {
            eventTarget.dispatchEvent(new Event("click", {bubbles: true}));
        }
    }

    function init1() {
        observeNodeIncrement();
        //首次链接分词监听
        addLinkListener();
        //选词监听
        document.addEventListener("selectionchange", getSelectedWord);
        //记录小球位置,避免拖拽时触发点击
        let startX;
        let startY;
        //为悬浮球添加鼠标按下事件,防止拖拽时触发点击
        document.getElementById("jav_ball").addEventListener("mousedown", (e) => {
            startX = e.clientX;
            startY = e.clientY;
        });
        //为悬浮球添加点击事件
        document.getElementById("jav_ball").addEventListener("click", (e) => {
            //先判断是否拖拽过,误差5px
            if (Math.abs(startX - e.clientX) > 5 || Math.abs(startY - e.clientY) > 5) {
                return;
            }
            //首次初始化
            if (!ready) {
                init2();
                ready = true;
            }
            //每次打开时重置placeholder
            textInput.placeholder = "使用前请先看说明!";
            //链接分词就不使用历史搜索词
            if (!isNewLinkWord) {
                //获取选词或历史
                textInput.value = historyKeyword;
                isNewLinkWord = false;
            }
            searchDialogObj.open();
        });

        //观察dom变化,因为个别网站会用xhr添加新的a标签,这部分链接将无法触发链接分词,dom增加节点时将刷新a标签的分词事件
        function observeNodeIncrement() {
            //观察document所有后代节点
            //Node是Element的父类,注意节点node和元素element是不同对象,node无法转成element
            const config = {childList: true, subtree: true};
            //延迟1秒重置标签,如果在一秒内又触发则取消执行,设置新的延迟任务,因为有时触发太频繁可能影响性能
            let observerTimer;
            let observer = new MutationObserver(nodeChangeCallback);
            observer.observe(document, config);
            console.log("dom观察:开始观察dom变化。");

            function nodeChangeCallback(mutationsList) {
                for (let mutation of mutationsList) {
                    if (mutation.addedNodes.length > 0) {
                        console.log("dom观察:检测到dom变化,并且是增加了节点。");
                    } else continue;
                    //只需要检测增加的节点,因为增加的a标签才需要重新绑定事件
                    for (let node of mutation.addedNodes) {
                        if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() !== "br"/*排除换行符,我们的notice常用到*/) {
                            //是元素增减,触发a标签事件重置,不遍历后代节点,直接重置
                            if (observerTimer) {
                                clearTimeout(observerTimer);
                                console.log("dom观察:清除定时任务。");
                            }
                            observerTimer = setTimeout(addLinkListener, 1000);
                            console.log("dom观察:增加的是元素或文本节点,可能存在a标签,一秒后重置所有a标签事件。");
                            return;
                        }
                    }
                }
            }
        }

        //https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
        function addLinkListener() {
            //获取页面所有链接,添加事件获取文本内容,移动端滑动触发,桌面端鼠标悬浮一会触发
            let links = document.getElementsByTagName("a");
            //链接分词监听,所有a标签
            for (let i = 0; i < links.length; i++) {
                //a标签有文字节点且有内容才添加事件
                if (!links[i].getAttribute("jav_listen") && /\S+/.test(links[i].textContent)) {
                    //为了node的增减,都需要先清除原先的事件,再添加
                    if (isAndroid) {
                        links[i].addEventListener("touchstart", getLinkText);
                    } else {
                        //桌面端鼠标悬浮触发
                        links[i].addEventListener("mousemove", rebuildLinkTimer/*鼠标移动的话不停重置定时器*/);
                        //离开时也清除定时器
                        links[i].addEventListener("mouseleave", clearLinkTimeout);
                    }
                    links[i].setAttribute("jav_listen", "true");
                }
            }
            console.log("dom观察:重置a标签事件完成。");

            function rebuildLinkTimer(event) {
                //在链接上移动鼠标需要重新设置定时器
                timeoutTimer && clearTimeout(timeoutTimer);
                timeoutTimer = setTimeout(() => {
                    getLinkText(event);
                }, 600);
            }

            function clearLinkTimeout() {
                timeoutTimer && clearTimeout(timeoutTimer);
            }

            function getLinkText(event) {
                textContent = event.target.textContent;
                if (separate(true)) isNewLinkWord = true;
            }
        }

        function getSelectedWord() {
            let selectWords = window.getSelection().toString();
            if (selectWords) {
                //需要清除首尾空格
                historyKeyword = removeSpaces(selectWords);
                //有一种情况,触发链接分词没有打开悬浮球,而是又进行了选词,这时应该使用最新操作的选词文本
                isNewLinkWord = false;
            }
        }
    }

    //更多功能的第二阶段初始化
    function init2() {
        buildCenMap();
        buildUncMap();
        buildSampleMap();
        settingDialogObj = new ModalDialog(settingCode(), {
            width: "250px",
            dialogPosition: searchDialogPosition
        });
        chooseDialogObj = new ModalDialog("<div id='jav_choose'></div>", {
            width: "320px",
            dialogPosition: "middle",
            canBeClose: false
        });
        //接收iframe发来的数据,绕过iframe安全机制
        window.addEventListener('message', e => {
            //需要判断一下内容,部分网站自己也会通信,会触发到这个方法
            if (typeof e.data === 'string' && e.data.includes("JavFrame>")) {
                appendNotice(e.data, false);
            }
        });
        //给搜索按钮添加事件
        btnMap.forEach((value, key) => {
            let btn = document.getElementById(key);
            btn.addEventListener("click", (event) => {
                //获取btn元素中的jav_url属性,这是在创建搜索模态框时设置好的
                let url = event.target.getAttribute("jav_url");
                let keyword = removeSpaces(textInput.value);
                //有搜索词
                //使用油猴的GM_openInTab(),因为window.open()存在个别网站打不开的情况
                if (keyword !== "" && !/^\s+$/.test(keyword)/*也不能全是空白符*/) {
                    openInTab(url.replace("%s", keyword), {insert: true, loadInBackground: false});
                }
                //没有搜索词
                else {
                    //其余跳到主页
                    let parts = url.split("/");
                    openInTab(parts[0] + "//" + parts[2], {insert: true, loadInBackground: false});
                }
            });
        });
        let btnGoArray = document.getElementsByClassName("jav_go");
        for (let go of btnGoArray) {
            if (go.getAttribute("jav_url")) {
                go.onclick = () => {
                    openInTab(go.getAttribute("jav_url"), {insert: true, loadInBackground: false});
                };
            }
        }
        //使用说明
        document.getElementById("jav_description").addEventListener("click", () => {
            openInTab("https://sleazyfork.org/zh-CN/scripts/470138#additional-info", {
                insert: true,
                loadInBackground: false
            });
        });
        //视频预览
        document.getElementById("jav_sample").addEventListener("click", sampleHandler);
        //清除输入框内容
        document.getElementById("jav_clear").addEventListener("click", () => {
            textInput.value = "";
            historyKeyword = "";
            textInput.placeholder = "请输入";
        });
        //复制
        document.getElementById("jav_copy").addEventListener("click", () => {
            GM_setClipboard(removeSpaces(textInput.value), "text");
            showNotice("已复制。");
            setTimeout(() => {
                hideNotice();
            }, 500);
        });
        //析出番号,手动粘贴后,点击按钮获取文本中的番号
        document.getElementById("jav_analyze").addEventListener("click", () => {
            textContent = textInput.value;
            separate(false);
        });
        //关闭按钮
        document.getElementById("jav_close").addEventListener("click", () => {
            searchDialogObj.close();
        });
        //获取页面中存在的视频的分辨率
        document.getElementById("jav_video_size").addEventListener("click", () => {
            //先获取当前页面视频信息
            videosInfo = getVideosInfo();
            searchDialogObj.close();
            showNotice(videosInfo);
            //发个空消息给所有子iframe,通知他们响应视频信息
            let iframes = document.getElementsByTagName('iframe');
            for (let i = 0; i < iframes.length; i++) {
                let iframe = iframes[i];
                if (iframe.contentWindow && iframe.contentWindow.postMessage) {
                    iframe.contentWindow.postMessage("JavSearch", '*');
                }
            }
        });
        document.getElementById("jav_setting").addEventListener("click", () => {
            settingDialogObj.open();
        });
        document.getElementById("mosaic_reduce_first").addEventListener("click", async () => {
            let mosaic_reduce_first = getTmValue("mosaic_reduce_first");
            setTmValue("mosaic_reduce_first", !mosaic_reduce_first);
            buildCenMap();
            document.getElementById("cen_pick_form").innerHTML = getSettingBtns(cenMap);
            document.getElementById("mosaic_reduce_first").textContent = "当前是:" + (!mosaic_reduce_first ? "无码破解" : "中文字幕") + "优先";
            let btnGoArray = document.getElementById("cen_pick_form").getElementsByClassName("jav_go");
            for (let go of btnGoArray) {
                if (go.getAttribute("jav_url")) {
                    go.onclick = () => {
                        openInTab(go.getAttribute("jav_url"), {insert: true, loadInBackground: false});
                    };
                }
            }
        });
        document.getElementById("jav_cen").addEventListener("click", searchHandler);
        document.getElementById("jav_unc").addEventListener("click", searchHandler);
        //模态框内输入框添加离焦事件更新按钮事件
        textInput.addEventListener("blur", () => {
            //保留输入历史
            historyKeyword = removeSpaces(textInput.value);
            textInput.placeholder = "请输入";
        });
    }

    //分词方法,blink表示是否分词后闪烁屏幕
    function separate(blink) {
        //输入框置空,显示placeholder的提示内容
        textInput.value = "";
        if (textContent === "") {
            textInput.placeholder = "文本内容不存在";
            return;
        }
        textContent = textContent.replaceAll("—", "-");
        //匹配正则数组
        let matchers = [
            /((?<![a-z0-9-_])[a-z]*[0-9]*[a-z]*|[0-9]{5,})((?<=[a-z0-9])[_-])([a-z]*[0-9]{2,5}(?![0-9]))/i,
            /(?<![a-z0-9])[a-z]+[0-9]{3,}(?![a-z0-9])/i,
            /(?<![a-z0-9])[0-9]{4,}(?![a-z0-9])/i
        ];
        let ids;
        //特殊片商处理(片商名称像番号)
        specialProducer = "";
        let specialProducerList = ["H4610", "H0930", "C0930"];
        for (let producer of specialProducerList) {
            if (new RegExp(producer, "i").test(textContent)) {
                //若视频预览功能匹配到响应格式的番号,将使用该片商的的预览地址
                specialProducer = producer;
                textContent = textContent.replace(producer, "").replace(producer.toLowerCase(), "");
                break;
            }
        }
        for (let i = 0; i < matchers.length; i++) {
            ids = textContent.match(matchers[i]);
            if (ids) {
                break;
            }
        }
        if (!ids) {
            textInput.placeholder = "链接或输入框文本中未找到番号";
            return false;
        }
        textInput.value = ids[0];
        historyKeyword = ids[0];
        if (blink) {
            blingObj.blink();
        }
        return true;
    }

    //处理cloudflare拦截
    async function checkCF(xhr) {
        let finalUrl = xhr.finalUrl;
        if (xhr.responseText.search(/you have been blocked/i) >= 0) {
            //被cloudflare拉黑
            appendNotice(finalUrl.split("/")[2] + "被cloudflare拉黑,请更换代理。", true);
            //1代表出错并继续
            return 1;
        } else if (xhr.responseText.search(/Enable JavaScript and cookies to continue/i) >= 0) {
            //需要真人验证
            appendNotice(finalUrl.split("/")[2] + "需要真人验证。爬取此网站要求篡改猴5.2或以上,并排除chrome128、129版本。", true);
            await sleep(1500);
            openInTab(finalUrl, {insert: true, loadInBackground: false});
            if (await choose("是否继续搜索?", "继续", "不用了")) {
                appendNotice("继续搜索...");
                //3代表正常并继续
                return 3;
            } else {
                hideNotice();
                //2代表停止
                return 2;
            }
        }
        //0代表未被拦截
        else return 0;
    }

    function checkInput(text) {
        if (text === "" || /^\s+$/.test(text)/*也不能全是空白符*/) {
            showNotice("未输入。");
            setTimeout(() => {
                hideNotice();
            }, 500);
            return false;
        }
        if (!/^[a-z0-9]+[-_]?[a-z0-9]+$/i.test(text) || /^[a-z]+$/i.test(text)) {
            showNotice("格式错误。");
            setTimeout(() => {
                hideNotice();
            }, 500);
            return false;
        }
        return true;
    }

    async function searchHandler(event) {
        let keyword = removeSpaces(textInput.value).replaceAll("—", "-");
        if (!checkInput(keyword)) {
            return;
        }
        //用于判断是否在过程中出错,如果出错就不关闭通知
        let error = false;
        //三次超时强制中断就会停止搜索
        let errorTimes = 0;
        let selected = [];
        let inputs;
        let currentMap;
        if (/jav_cen/.test(event.target.id)) {
            showNotice("正在搜索...<br/>此按钮搜索如aaa-111、111aaaa-111等有码格式的番号,中文字幕和无码破解版本优先。");
            inputs = document.getElementById("cen_pick_form").getElementsByTagName("input");
            currentMap = cenMap;
        } else {
            showNotice("正在搜索...<br/>此按钮搜索如111111、111111-111、n1111、heyzo-111等无码格式的番号。");
            inputs = document.getElementById("unc_pick_form").getElementsByTagName("input");
            currentMap = uncMap;
        }
        for (let input of inputs) {
            if (input.checked) {
                selected.push(input.id);
            }
        }
        let beFound = false;
        //若没有字幕或无码破解版本但存在普通视频,将链接保存,在指定搜索结果都没找到到时使用
        let savingUrl = "";
        await checkUser();
        for (let key of selected) {
            if (/---/.test(key)) {
                //指定引擎搜索不到后,如果前面有普通版本也打开
                if (savingUrl) {
                    //打开特殊引擎的保留地址
                    hideNotice();
                    //凡是要隐藏通知然后打开新窗口的,都要等动画结束再打开,不然移动端回到页面会继续未完成动画,造成残影
                    //延迟时间与模态框淡出持续事件相等即可
                    setTimeout(() => {
                        openInTab(savingUrl, {insert: true, loadInBackground: false});
                    }, 300);
                    return;
                }
                //分割线,不执行搜索
                continue;
            }
            beFound = await crawl(currentMap.get(key), keyword);
            if (beFound) break;
        }
        if (!beFound) {
            appendNotice("未找到结果,将尝试谷歌搜索。", true);
            setTimeout(() => {
                openInTab("https://www.google.com/search?q=%s%20jav".replace("%s", keyword), {
                    insert: true,
                    loadInBackground: false
                });
            }, 1000);
        }

        function crawl(engine, id) {
            let url = engine.url;
            let urlSplit = url.split("/");
            let topLevelSite = urlSplit[0] + "//" + urlSplit[2];
            if (/missav/.test(topLevelSite)) {
                //需要将mgs格式改为常规格式
                if (/^[0-9]+[a-z]+-[0-9]+$/i.test(id)) {
                    id = id.replace(/[0-9]*(?=[a-z])/i, "");
                }
            }
            url = url.replace("%s", id);
            let headers = {};
            if (/7mmtv/.test(topLevelSite)) {
                headers = {"content-type": "application/x-www-form-urlencoded"};
            }
            let timeout = 4000;
            //已经超时强制中断的不再显示请求超时的通知
            let aborted = false;
            return new Promise((resolve) => {
                let connectTimeout = true;
                let promise = GM_xmlhttpRequest({
                    method: engine.reqMethod,
                    url: url,
                    //data为空时必须传null
                    data: engine.payload ? engine.payload.replace("%s", id) : null,
                    timeout: timeout,
                    headers: headers,
                    cookiePartition: {
                        topLevelSite: topLevelSite
                    },
                    onload: async function (xhr) {
                        connectTimeout = false;
                        let finalUrl = xhr.finalUrl;
                        let finalUrlSplit = finalUrl.split("/");
                        let currentHost = finalUrlSplit[0] + "//" + finalUrlSplit[2];
                        try {
                            let _id = id;
                            //不支持模糊搜索,需要完全匹配
                            if (/[a-z]/i.test(_id)) {
                                _id = `(?<![a-z])${_id}(?![0-9])`;
                            } else {
                                //如果是纯数字的番号,不要只匹配到几个数字
                                _id = `(?<![a-z0-9])${_id}(?![0-9])`;
                            }
                            //7mm
                            if (/7mmtv/.test(finalUrl)) {
                                //<a target="_top" href="https://7mmtv.sx/zh/chinese_content/37171/WAAA-167.html"
                                let urlReg = new RegExp(`${currentHost}/zh/[a-z]+_content/[^/]*/[^\\.]*${_id}.html`, "gi");
                                let matchArray = xhr.responseText.matchAll(urlReg);
                                for (let match of matchArray) {
                                    if (/jav_unc/.test(event.target.id)) {
                                        hideNoticeResolveAndOpen(match[0]);
                                        return;
                                    }
                                    if (!savingUrl) {
                                        savingUrl = match[0];
                                    }
                                    if (match[0].search(/chinese/) >= 0) {
                                        hideNoticeResolveAndOpen(match[0]);
                                        return;
                                    }
                                }
                                resolve(false);
                            }
                            //jav777
                            else if (/jav777/.test(finalUrl)) {
                                //https://www.jav777.xyz/waaa-111-絕頂舔鮑狂熱-木下日葵有碼中文字幕.html
                                let regString = `/[^.]*${_id}[^.]*\\.html`;
                                let urls = xhr.responseText.match(new RegExp(regString, "i"));
                                if (urls) {
                                    hideNoticeResolveAndOpen(currentHost + urls[0]);
                                } else {
                                    resolve(false);
                                }
                            }
                            //ggjav
                            else if (/ggjav/.test(finalUrl)) {
                                //<div class="item_title"><a class="gray_a" href="/main/video?id=5907">Carib 120311-877 朝倉ことみ 痴漢電車</a></div>
                                let reg = new RegExp(`href="(/main/video\\?id=[0-9]+)">[^<]*${_id}[^<]*</a></div>`, "ig");
                                let matches = xhr.responseText.matchAll(reg);
                                for (let match of matches) {
                                    if (/jav_unc/.test(event.target.id)) {
                                        hideNoticeResolveAndOpen(currentHost + match[1]);
                                        return;
                                    }
                                    if (!savingUrl) {
                                        savingUrl = currentHost + match[1];
                                    }
                                    if (match[0].search(/中文字幕/) >= 0) {
                                        hideNoticeResolveAndOpen(currentHost + match[1]);
                                        return;
                                    }
                                }
                                resolve(false);
                            }
                            //supjav
                            else if (/supjav/.test(finalUrl)) {
                                switch (await checkCF(xhr)) {
                                    case 1:
                                        error = true;
                                        resolve(false);
                                        return;
                                    case 2:
                                        resolve(true);
                                        return;
                                    case 3:
                                        resolve(false);
                                        return;
                                }
                                //case为0时将继续运行
                                //href="https://supjav.com/zh/147335.html" title="FC2PPV 2753411 * Apricot...
                                let reg = new RegExp(`"(${currentHost}/zh/[0-9]+.html)" title="[^"]*${_id}[^"]*"`, "gi");
                                let matches = xhr.responseText.matchAll(reg);
                                for (let match of matches) {
                                    if (/jav_unc/.test(event.target.id)) {
                                        hideNoticeResolveAndOpen(match[1]);
                                        return;
                                    }
                                    if (!savingUrl) {
                                        savingUrl = match[1];
                                    }
                                    if (match[0].search(/无码破解|无码流出|無修正/) >= 0) {
                                        hideNoticeResolveAndOpen(match[1]);
                                        return;
                                    }
                                }
                                //这网站的中文字幕只是顺便搜索的,并非主力搜索引擎,所以在字幕优先时并不一定搜索到这个
                                for (let match of matches) {
                                    if (match[0].search(/中文字幕/) >= 0) {
                                        hideNoticeResolveAndOpen(match[1]);
                                        return;
                                    }
                                }
                                resolve(false);
                            }
                            //missav
                            else if (/missav/.test(finalUrl)) {
                                if (/排序/.test(xhr.responseText)) {
                                    //有排序选项表示存在结果
                                    /*
                                    <a href="https://missav.com/dm47/roe-142" alt="???-roe-142-???">
                                    <span class="...">
                                    中文字幕
                                    </span>
                                    */
                                    let reg = new RegExp(`<a href="([^"]*)" alt="[^"]*${_id}[^"]*">\\s<span[^>]*>[^<]*</span>`, "gi");
                                    let matches = xhr.responseText.matchAll(reg);
                                    for (let match of matches) {
                                        if (/jav_cen/.test(event.target.id)) {
                                            if (!savingUrl) {
                                                savingUrl = match[1];
                                            }
                                            if (/中文字幕/.test(match[0])) {
                                                hideNoticeResolveAndOpen(match[1]);
                                                return;
                                            }
                                        } else {
                                            hideNoticeResolveAndOpen(match[1]);
                                            return;
                                        }
                                    }
                                    resolve(false);
                                } else {
                                    resolve(false);
                                }
                            }
                            //jable
                            else if (/jable/.test(finalUrl)) {
                                if (/search/.test(finalUrl)) {
                                    //<a href="https://jable.tv/videos/miab-304/">MIAB-304...
                                    let regString = `<a href="([^"]*)">[^<]*${_id}[^<]*<`;
                                    let urls = xhr.responseText.match(new RegExp(regString, "i"));
                                    if (urls) {
                                        resolve(await crawl({url: urls[1], reqMethod: "get"}, ""));
                                    } else {
                                        resolve(false);
                                    }
                                } else if (/videos/.test(finalUrl)) {
                                    let cn = xhr.responseText.match(new RegExp("已更新至中文字幕版"));
                                    if (cn) {
                                        hideNoticeResolveAndOpen(finalUrl);
                                    } else {
                                        savingUrl = finalUrl;
                                        resolve(false);
                                    }
                                }
                            }
                            //guru
                            else if (/guru/.test(finalUrl)) {
                                /*
                                <a href="https://jav.guru/320706/101323-001-carib...">
                                <img src="..." alt="[101323-001-CARIB] Caribbeancom 101323-001 ...">
                                </a>
                                * */
                                let regString = `<a href="([^"]*${_id}[^"]*)">\\s<img`;
                                let urls = xhr.responseText.match(new RegExp(regString, "i"));
                                if (urls) {
                                    hideNoticeResolveAndOpen(urls[1]);
                                } else {
                                    resolve(false);
                                }
                            }
                            //njav
                            else if (/njav/.test(finalUrl)) {
                                switch (await checkCF(xhr)) {
                                    case 1:
                                        error = true;
                                        resolve(false);
                                        return;
                                    case 2:
                                        resolve(true);
                                        return;
                                    case 3:
                                        resolve(false);
                                        return;
                                }
                                /*
                                * <a href="v/caribbeancom-101323-001"
                                * */
                                let regString = `<a href="(v/[^"]*${_id}[^"]*)"`;
                                let urls = xhr.responseText.match(new RegExp(regString, "i"));
                                if (urls) {
                                    hideNoticeResolveAndOpen(currentHost + "/zh/" + urls[1]);
                                } else {
                                    resolve(false);
                                }
                            }
                            //today
                            else if (/\.today/.test(finalUrl)) {
                                //https://javhd.today/search/video/?s=SKMJ-073
                                //href="/59014/skmj-073-studio.../" title="SKMJ-073 Studio
                                let reg = new RegExp(`href="([^"]*)" title="[^"]*${_id}[^"]*"`, "i");
                                let matchResult = xhr.responseText.match(reg);
                                if (matchResult) {
                                    hideNoticeResolveAndOpen(currentHost + matchResult[1]);
                                } else {
                                    resolve(false);
                                }
                            }
                            //没有匹配域名,被重定向到其他域名,搜索失败
                            else {
                                error = true;
                                appendNotice("错误:被重定向,请更换代理。原域名:"
                                    + urlSplit[2] + ",重定向域名:" + finalUrlSplit[2], true);
                                resolve(false);
                            }
                        } catch (e) {
                            error = true;
                            appendNotice(`错误:${url},请联系开发者。<br/>${e.name}:${e.message}`, true);
                            resolve(false);
                        }
                    },
                    ontimeout: function () {
                        error = true;
                        connectTimeout = false;
                        if (!aborted) {
                            appendNotice("超时:" + urlSplit[2], true);
                        }
                        resolve(false);
                    },
                    onerror: function () {
                        error = true;
                        connectTimeout = false;
                        appendNotice("错误:" + urlSplit[2] + ",机场不行或网站挂了。", true);
                        resolve(false);
                    }
                });
                setTimeout(() => {
                    promise.abort();
                    if (connectTimeout) {
                        error = true;
                        aborted = true;
                        appendNotice("超时强制中断:" + urlSplit[2], true);
                        errorTimes++;
                        if (errorTimes >= 3) {
                            appendNotice("你代理没开吧?", true);
                            resolve(true);
                            return;
                        }
                        resolve(false);
                    }
                }, timeout);

                //注意定义方法的位置,resolve方法需要包含在内
                function hideNoticeResolveAndOpen(url) {
                    if (!error) {
                        hideNotice();
                    }
                    resolve(true);
                    setTimeout(() => {
                        openInTab(url, {insert: true, loadInBackground: false});
                    }, 300);
                }
            });
        }
    }

    async function sampleHandler() {
        let id = removeSpaces(textInput.value).toLowerCase().replaceAll("—", "-");
        if (!checkInput(id)) {
            return;
        }
        //正则
        let common_reg = /^[a-z]+[0-9]*[a-z]*-[a-z]*[0-9]+$/i;
        let heyzo_reg = /^heyzo-[0-9]+$/i;
        let mgs_reg = /^[0-9]+[a-z]+-[0-9]+$/i;
        let fc2_reg = /^[0-9]{6,7}$/;
        let carib_reg = /^[0-9]{6}-[0-9]{3}$/;
        let heyzo_k8_tokyoHot4_reg = /^[0-9]{4}$/;
        let paco_1p_reg = /^[0-9]{6}_[0-9]{3}$/;
        let _10m_reg = /^[0-9]{6}_[0-9]{2}$/;
        let nyoshin_tokyoHotN_reg = /^n[0-9]{4}$/i;
        let xxxav_reg = /^[0-9]{5}$/;
        let other_reg = /^[a-z]+[0-9]+$/i;
        let gachi_reg = /gachi/i;
        //网址
        /*
        有些是搜索页,有些直接是详情页
        基本涵盖所有番号的视频预览自动播放
        全是get请求
        */
        let jav24 = "https://www.jav24.com/watch/www.mgstage.com/product/product_detail/%s/";
        let mgs = "https://www.mgstage.com/product/product_detail/%s/";
        let fc2 = "https://javten.com/tw/search?kw=%s";//自动重定向,比fc2官网更快更稳
        let carib = "https://en.caribbeancom.com/eng/moviepages/%s/index.html";
        let caribpr = "https://en.caribbeancompr.com/moviepages/%s/index.html";
        let _10musume = "https://en.10musume.com/movies/%s/";
        let _1pondo = "https://en.1pondo.tv/movies/%s/";
        let heyzo = "https://en.heyzo.com/moviepages/%s/index.html";
        let pacopacomama = "https://en.pacopacomama.com/movies/%s/";
        let nyoshin = "https://en.nyoshin.com/moviepages/%s/index.html";
        let kin8tengoku = "https://en.kin8tengoku.com/moviepages/%s/index.html";
        let xxxav = "https://www.xxx-av.com/mov/movie/%s/";
        let tokyoHot_result = "https://my.tokyo-hot.com/product/?q=%s";
        let tokyoHot_sample = "https://my.cdn.tokyo-hot.com/media/samples/%s.mp4";
        let ave = "https://www.aventertainments.com/search_Products.aspx?languageID=1&keyword=%s";
        let specialProducers = {
            "H4610": "https://en.h4610.com/moviepages/%s/index.html",
            "H0930": "https://en.h0930.com/moviepages/%s/index.html",
            "C0930": "https://en.c0930.com/moviepages/%s/index.html"
        };
        let notice_text = "正在搜索...<br/>搜索前请先确认番号的准确性,fc2的番号只搜索数字部分即可。";
        //用于部分存在相同番号的片商,记录结果数量
        let opened = 0;
        //爬取过程中出错将不会关闭通知模态框
        let error = false;
        //三次超时强制中断就会停止搜索
        let errorTimes = 0;
        //用于记录常规番号的搜索结果中包含图片的首个结果,在没有预览视频时使用
        let imgUrl;
        //如abc-111
        if (common_reg.test(id) && !heyzo_reg.test(id) && !gachi_reg.test(id)) {
            await checkUser();
            //ave常规格式番号前缀,优先爬取,提升搜索速度
            const aveIds = ["sky", "skyhd", "cwp", "cwpbd", "lldv", "laf", "lafbd", "hey", "ccdv", "ssdv",
                "smd", "smdv", "smbd", "pt", "bt", "s2m", "s2mbd", "mmdv", "mcdv", "mcbd", "mxx",
                "drc", "drg", "drgbd", "dsam", "dsamd", "dsambd", "rhj", "jav", "pink"];
            let prefix = id.split("-")[0];
            showNotice(notice_text);
            for (let aveId of aveIds) {
                if (aveId === prefix.toLowerCase()) {
                    //前缀与aveId匹配,直接在ave搜索,然后终止方法
                    if (!await crawl(ave)) {
                        nothing();
                    }
                    return;
                }
            }
            //常规有码的搜索引擎可以设置参不参与搜索
            let brands = [];
            let inputs = document.getElementById("sample_pick_form").getElementsByTagName("input");
            for (let input of inputs) {
                if (input.checked) {
                    brands.push(sampleMap.get(input.id).url);
                }
            }
            brands.push(tokyoHot_result, ave/*没归纳的无码*/);
            await crawlList(brands, true);
        }
        //如11aaa-11
        else if (mgs_reg.test(id)) {
            await checkUser();
            showNotice(notice_text);
            await crawlList([jav24, mgs/*需要日本ip*/]);
        }
        //6到7位纯数字
        else if (fc2_reg.test(id)) {
            //这种是没有同类番号,也不需要根据响应结果判断搜索结果是否存在,就直接打开
            replaceAndOpen(fc2, false);
        }
        //6位-3位
        else if (carib_reg.test(id)) {
            replaceAndOpen(carib);
        }
        //6位_2位
        else if (_10m_reg.test(id)) {
            replaceAndOpen(_10musume);
        }
        //纯4位数字的heyzo等
        else if (heyzo_k8_tokyoHot4_reg.test(id)) {
            await checkUser();
            showNotice(notice_text);
            //每个有结果的都打开,
            await crawl(heyzo);
            await crawl(kin8tengoku);
            await crawl(tokyoHot_sample);
            if (opened === 0) {
                //一个没找到
                nothing();
            }
        }
        //纯5位数字
        else if (xxxav_reg.test(id)) {
            await checkUser();
            showNotice(notice_text);
            if (!await crawl(xxxav)) {
                nothing();
            }
        }
        //带前缀heyzo-的
        else if (heyzo_reg.test(id)) {
            //取数字部分
            id = id.split("-")[1];
            replaceAndOpen(heyzo);
        }
        //6位_3位
        else if (paco_1p_reg.test(id)) {
            await checkUser();
            showNotice("正在搜索...<br/>一本道存在结果将不爬取加勒比。");
            await crawlList([_1pondo, pacopacomama, caribpr]);
        }
        //n开头加4位数字
        else if (nyoshin_tokyoHotN_reg.test(id)) {
            await checkUser();
            showNotice(notice_text);
            //nyoshin都没没什么源,番号却老重复,故优先TokyoHot,没结果再测nyoshin
            await crawlList([tokyoHot_result, nyoshin]);
        }
        //其他字母加数字形式
        else if (other_reg.test(id) && !gachi_reg.test(id)) {
            //明确片商则无需重复打开
            if (specialProducer) {
                await replaceAndOpen(specialProducers[specialProducer]);
                specialProducer = "";
                return;
            }
            showNotice(notice_text);
            await checkUser();
            //TokyoHot的搜索结果会找不着,得在视频链接拼接试试
            if (!await crawl(tokyoHot_result, {"user-agent": "windows"})) {
                //即使找到视频也不知道详情页地址,直接播放即可
                await crawl(tokyoHot_sample);
            }
            if (opened > 0) {
                //如果在TokyoHot搜到视频就不再搜索了,番号一般不和下面的特殊片商重复
                return;
            }
            //这三个片商番号极多重复
            await crawl(specialProducers.H4610);
            await crawl(specialProducers.H0930);
            await crawl(specialProducers.C0930);
            if (opened === 0) {
                //一个没找到
                nothing();
            }
        }
        //其余形式未收录
        else {
            //其他番号尚未收录
            if (gachi_reg.test(id)) {
                showNotice("该片商官网已关闭。");
            } else {
                showNotice("输入错误或未收录该番号。");
            }
        }

        function replaceAndOpen(url, autoplay = true) {
            hideNotice();
            setTmValue("autoplay", autoplay);
            window.setTimeout(() => {
                openInTab(url.replace("%s", id), {insert: true, loadInBackground: false});
            }, 300);
        }

        async function crawlList(brands, needImg = false/*cen需要预览图,unc不需要*/) {
            let beFound;
            for (let brand of brands) {
                beFound = await crawl(brand);
                if (beFound) {
                    //找到一个就停
                    break;
                }
            }
            if (!beFound) {
                //一个视频都没找到就看预览图
                if (needImg && imgUrl) {
                    setTmValue("show_img", true);
                    hideNoticeAndOpen(imgUrl, false);
                } else {
                    nothing();
                }
                return false;
            } else return true;
        }

        //爬取搜索结果或详情页中是否包含预览视频或图片
        function crawl(url) {
            let urlSplit = url.split("/");
            let topLevelSite = urlSplit[0] + "//" + urlSplit[2];
            let _id = id;
            if (/mgstage\.com/.test(topLevelSite)) {
                _id = _id.toUpperCase();
            }
            url = url.replace("%s", _id);
            //默认3秒超时
            let timeout = 3000;
            if (/tokyo-hot.*samples/.test(url)) {
                //视频链接,2秒内没404就表示存在
                timeout = 2000;
            } else if (/aventertainments.*search/.test(url)) {
                //响应太慢了,实际上可以访问
                timeout = 6000;
            }
            let headers = {};
            if (/my.tokyo-hot.com\/product/.test(url)) {
                headers = {"user-agent": "windows"};
            }
            let cookie = "";
            if (/mgstage\.com/.test(topLevelSite)) {
                cookie = "adc=1;";
            }
            //已经超时强制中断的不再显示请求超时的通知
            let aborted = false;
            return new Promise((resolve) => {
                let connectTimeout = true;
                let promise = GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    timeout: timeout,
                    headers: headers,
                    cookie: cookie,
                    cookiePartition: {
                        topLevelSite: topLevelSite
                    },
                    onload: async function (xhr) {
                        connectTimeout = false;
                        let finalUrl = xhr.finalUrl;
                        try {
                            /*
                            需要正则匹配的话,都得从开发者工具的网络工具中查看源码,不能在元素工具查看,不然会出现错误
                            注意id如cap-111和ap-11是不一样的
                            注意.不能替代[\\s\\S],同时浏览器有格式化功能,是否存在换行符需要看源码格式
                            */
                            //ave结果,常规格式无码
                            if (/aventertainments/i.test(finalUrl)) {
                                /*
                                详情页https://www.aventertainments.com/40676/2/29/product_lists
                                商品番号 SMBD-18
                                源码 <span class="title">商品番号</span><span class="tag-title">SMBD-18</span>
                                */
                                if (/lists/.test(finalUrl)) {
                                    let searchWord = new RegExp(`<span class="tag-title">${_id}</span>`, "i");
                                    if (xhr.responseText.search(searchWord) === -1) {
                                        resolve(false);
                                    } else {
                                        hideNoticeAndOpen(finalUrl);
                                        resolve(true);
                                    }
                                }
                                /*搜索页 https://www.aventertainments.com/search_Products.aspx?keyword=SMBD-18
                                single-slider-product是存在结果时响应中存在的class*/
                                else if (xhr.responseText.search(/single-slider-product/i) === -1) {
                                    resolve(false);
                                }
                                /*
                                即使有结果,该结果未必是搜索的番号,比如搜索bc会出现abc的结果
                                并且从响应中无法判断是否完全匹配,需要再到详情页比较避免搜索错误,这也是ave搜索慢的原因
                                搜索结果中的详情页url https://www.aventertainments.com/40676/2/29/product_lists
                                */
                                else {
                                    //爬取所有结果是否是与id匹配的
                                    let matchArr = xhr.responseText.match(/https:\/\/www.aventertainments.com.*?product_lists/gi);
                                    let urlSet = new Set();
                                    for (let matchArrElement of matchArr) {
                                        //很多有重复的
                                        urlSet.add(matchArrElement);
                                    }
                                    let beFound;
                                    for (let url of urlSet) {
                                        beFound = await crawl(url);
                                        if (beFound) break;
                                    }
                                    if (beFound) {
                                        resolve(true);
                                    } else resolve(false);
                                }
                            }
                            //trailer结果
                            else if (/trailer/i.test(finalUrl)) {
                                //详情页 https://javtrailers.com/video/ssis00411
                                if (/video/.test(finalUrl)) {
                                    if (xhr.responseText.search("videoPlayerContainer") === -1) {
                                        //image-container 图片的class,响应中有一个是css的选择器,所以超过一个就表示有图片
                                        if (!imgUrl && xhr.responseText.search("gallery-button") > 0) {
                                            //记录图片地址
                                            imgUrl = finalUrl;
                                        }
                                        resolve(false);
                                    } else {
                                        hideNoticeAndOpen(finalUrl);
                                        resolve(true);
                                    }
                                }
                                /*结果页 https://javtrailers.com/search/SSIS-411
                                <img alt="SSIS-411 jav" class="card-img-top video-image"
                                "id空格jav在结果中只存在一个,开头双引号不能去掉,不然会有三个*/
                                else if (xhr.responseText.search(new RegExp(`"${_id} jav`, "i")) !== -1) {
                                    //搜索到了,获取地址爬取 <div class="card-container">...href="/video/ssis00411"...没有换行符,是浏览器格式化了...alt="SSIS-411 jav"
                                    let matchArray = xhr.responseText.match(new RegExp(`"(/video/[^"]*)"(.(?!card-container))*"${_id} jav"`, "i"));
                                    if (matchArray) {
                                        //使用日语网页
                                        resolve(await crawl(topLevelSite + "/ja" + matchArray[1]));
                                    }
                                } else {
                                    resolve(false);
                                }
                            }
                            //land结果,不需要判断搜索结果,有会重定向
                            else if (/jav\.land/i.test(finalUrl)) {
                                switch (await checkCF(xhr)) {
                                    case 1:
                                        error = true;
                                        resolve(false);
                                        return;
                                    case 2:
                                        resolve(true);
                                        return;
                                    case 3:
                                        resolve(false);
                                        return;
                                }
                                if (/Too many connections/i.test(xhr.responseText)) {
                                    error = true;
                                    appendNotice("错误:jav.land请求过多被拦截。", true);
                                    resolve(false);
                                    return;
                                }
                                //jav.land搜索到会直接重定向到product页面,如果是请求地址没变代表没有搜索到 https://jav.land/tw/id_search.php?keys=WAAA-164
                                if (/id_search/i.test(finalUrl)) {
                                    //没有搜索结果
                                    resolve(false);
                                }
                                //如果请求被重定向表示有结果,https://jav.land/tw/movie/javw6eojvep.html
                                else if (/movie/i.test(finalUrl)) {
                                    if (xhr.responseText.search("play_sample") !== -1) {
                                        hideNoticeAndOpen(finalUrl);
                                        resolve(true);
                                    } else {
                                        //<span id="waterfall"><a href="地址"><img src="地址" /></a>等等,还有一个waterfall字符串存在于<script>标签内
                                        if (!imgUrl && xhr.responseText.match(new RegExp("waterfall", "gi")).length === 2) {
                                            imgUrl = finalUrl;
                                        }
                                        resolve(false);
                                    }
                                }
                            }
                            //jav24
                            else if (/jav24/i.test(finalUrl)) {
                                //mgs格式的视频预览
                                if (/mgstage/i.test(finalUrl)) {
                                    if (xhr.status === 410) {
                                        resolve(false);
                                    } else {
                                        resolve(true);
                                        hideNoticeAndOpen(finalUrl);
                                    }
                                    return;
                                }
                                //https://www.jav24.com/?q=EROFV-130
                                //"/watch...."...my__product__code...EROFV-130<span
                                let _24reg = new RegExp(`"(/watch[^"]*)"(.(?!/watch/))*${_id}<span`, "i");
                                let match = xhr.responseText.match(_24reg);
                                if (match) {
                                    hideNoticeAndOpen(topLevelSite + match[1]);
                                    resolve(true);
                                } else {
                                    resolve(false);
                                }
                            }
                            //mgstage
                            else if (/mgstage\.com/i.test(finalUrl)) {
                                if (/product/i.test(finalUrl)) {
                                    //不存在会跳转到主页
                                    hideNoticeAndOpen(url);
                                    resolve(true);
                                } else {
                                    resolve(false);
                                }
                            }
                            //javdb
                            else if (/javdb/i.test(finalUrl)) {
                                //搜索结果页 https://javdb.com/search?q=pts-437
                                if (/search/.test(finalUrl)) {
                                    //日本ip会被拒绝,或者响应被限制的提示文本
                                    if (xhr.status === 403 || xhr.responseText.search("prohibited"/*禁止*/) !== -1) {
                                        error = true;
                                        appendNotice("错误:javdb不能使用日本节点。", true);
                                        resolve(false);
                                        return;
                                    }
                                    //<a href="/v/ppARk"...包含换行符...<strong>PTS-437</strong>
                                    let matchArr;
                                    matchArr = xhr.responseText.match(new RegExp(`"(/v/[^"]*)"([\\s\\S](?!/v/))*>${_id}<`, "i"));
                                    if (!matchArr) {
                                        resolve(false);
                                        return;
                                    }
                                    //找到了,去爬取详情页有没有视频存在
                                    let uri = matchArr[1];
                                    resolve(await crawl(topLevelSite + uri));
                                }
                                //详情页 https://javdb.com/v/DYvxa 可见地址中不包含id
                                else if (/\/v\//.test(finalUrl)) {
                                    //详情页,存在视频时有个预告片a标签,preview-video-container是它的class
                                    if (xhr.responseText.search("preview-video-container") !== -1) {
                                        hideNoticeAndOpen(finalUrl);
                                        resolve(true);
                                    } else {
                                        //data-caption是包含图片的a标签的特有属性,存在即有图片
                                        if (!imgUrl && xhr.responseText.search(/data-caption/i) !== -1) {
                                            //详情页存在图片,记录
                                            imgUrl = finalUrl;
                                        }
                                        //往后搜索
                                        resolve(false);
                                    }
                                }
                            }
                            //TokyoHot结果,很多乱七八糟的番号需要搜索,地址中需要包含product,拼接番号访问sample的跳过
                            else if (/tokyo-hot.*q=/i.test(finalUrl)) {
                                //搜索页 https://my.tokyo-hot.com/product/?q=n1658
                                if (xhr.responseText.search(new RegExp("Product ID: " + _id, "i")) !== -1) {
                                    //<a href="/product/n1658/" class="rm">...含有换行符...(Product ID: n1658)
                                    let product_path = xhr.responseText.match(new RegExp(`(/product/[^/]*/)([\\s\\S](?!/product/))*ID: ${_id}`, "i"))[1];
                                    hideNoticeAndOpen("https://my.tokyo-hot.com" + product_path);
                                    opened++;
                                    resolve(true);
                                } else {
                                    resolve(false);
                                }
                            }
                            //nyoshin,拼接番号后请求,不存在结果会重定向到首页
                            else if (/nyoshin/i.test(finalUrl)) {
                                //https://en.nyoshin.com/moviepages/n2348/index.html
                                if (/moviepages/i.test(finalUrl)) {
                                    hideNoticeAndOpen(url);
                                    resolve(true);
                                } else {
                                    resolve(false);
                                }
                            }
                            //xxxav,拼接方式请求,无结果重定向到首页
                            else if (/xxx-av/i.test(finalUrl)) {
                                //https://en.xxx-av.com/mov/movie/18059/
                                if (/movie/.test(finalUrl)) {
                                    hideNoticeAndOpen(url);
                                    resolve(true);
                                } else {
                                    resolve(false);
                                }
                            }
                            //其他请求只要不是404就打开
                            else if (xhr.status !== 404) {
                                //这些片商番号重复的太多,搜索到也不能停,统计打开网页个数,因为最后要判断是否有找到结果
                                if (/heyzo|kin8tengoku|tokyo-hot|h4610|c0930|h0930/i.test(finalUrl)) {
                                    opened++;
                                    //不需要传参判断是否找到,都得测
                                    resolve();
                                } else resolve(true);
                                hideNoticeAndOpen(url);
                            }
                            //404未找到
                            else {
                                resolve(false);
                            }
                        } catch (e) {
                            error = true;
                            appendNotice(`错误:${url},请联系开发者。<br/>${e.name}:${e.message}`, true);
                            resolve(false);
                        }
                    },
                    onerror: function () {
                        error = true;
                        connectTimeout = false;
                        appendNotice("错误:" + urlSplit[2] + ",机场不行或网站挂了。", true);
                        resolve(false);
                    },
                    ontimeout: function () {
                        error = true;
                        connectTimeout = false;
                        //访问的如果是视频资源,响应要很久才结束,所以两秒内没有响应404则默认存在该视频
                        if (/tokyo-hot.*samples/.test(url)) {
                            opened++;
                            hideNoticeAndOpen(url);
                        } else {
                            if (!aborted) {
                                appendNotice("超时:" + urlSplit[2], true);
                            }
                            resolve(false);
                        }
                    },
                    onprogress: function () {
                        connectTimeout = false;
                    }
                });
                setTimeout(() => {
                    promise.abort();
                    if (connectTimeout) {
                        error = true;
                        aborted = true;
                        appendNotice("超时强制中断:" + urlSplit[2], true);
                        errorTimes++;
                        if (errorTimes >= 3) {
                            appendNotice("你代理没开吧?", true);
                            resolve(true);
                            return;
                        }
                        resolve(false);
                    }
                }, timeout);
            });
        }

        function nothing() {
            appendNotice("预览视频或图片皆未找到。", true);
        }

        //关闭模态框通知并打开指定页面
        function hideNoticeAndOpen(url, autoplay = true) {
            //db等需要自动点击,仅在第一次跳转时自动点击,再刷新就不用了
            setTmValue("autoplay", autoplay);
            if (!error) {
                hideNotice();
            }
            window.setTimeout(() => {
                openInTab(url, {insert: true, loadInBackground: false});
            }, 300);
        }
    }

    async function checkUser() {
        //首次使用设置指引
        if (!getTmValue("old_user")) {
            showNotice("首次使用需要在弹出页面点选“总是允许全部域名”,将在3秒后弹出...");
            await sleep(3000);
            setTmValue("old_user", true);
        }
    }

    //全局通知方法
    function showNotice(notice) {
        document.getElementById("jav_notice").innerHTML = notice;
        noticeDialogObj.open();
    }

    function hideNotice() {
        noticeDialogObj.close();
        document.getElementById("jav_notice").innerHTML = "";
    }

    function appendNotice(notice, warning = false) {
        let noticedDiv = document.getElementById("jav_notice");
        if (warning) {
            notice = "<span class='eerror'>" + notice + "</span>";
        }
        noticedDiv.innerHTML += (noticedDiv.innerHTML ? "<br/>" : "") + notice;
        if (!noticeDialogObj.opened) {
            noticeDialogObj.open();
        }
    }

    //生成一个选择框,点击第一个按钮返回true,第二个为false
    async function choose(words, name1, name2) {
        document.getElementById("jav_choose").innerHTML = words + "<br/><div class='bbtn llink'>" + name1 +
            "</div><div class='bbtn cclose'>" + name2 +
            "</div>";
        let result;
        let btns = document.getElementById("jav_choose").getElementsByClassName("bbtn");
        btns[0].addEventListener("click", () => {
            chooseDialogObj.close();
            result = true;
        });
        btns[1].addEventListener("click", () => {
            chooseDialogObj.close();
            result = false;
        });
        chooseDialogObj.open();
        //阻塞,配合“点击背景无法关闭模态框”,等待点击完成才可以进行其他操作
        while (true) {
            if (result !== undefined) {
                return result;
            }
            await sleep(500);
        }
    }

    function removeSpaces(s) {
        return s.replace(/^\s+/, '').replace(/\s+$/, '');
    }

    function getVideosInfo() {
        let videos = document.getElementsByTagName("video");
        let num = videos.length;
        let info = `${top === window ? "Top>" : "JavFrame>"}${location.host}(${num})${num > 0 ? ":" : ""}`;
        let count = 0;
        for (let i = 0; i < num; i++) {
            if (videos[i].readyState !== 0) {
                let video = videos[i];
                info += `<br/>video${i}:${video.videoWidth}*${video.videoHeight}`;
                count++;
            }
        }
        if (count === 0 && num > 0) {
            info += "<br/>null";
        }
        return info;
    }

    //同步等待若干时间
    function sleep(time) {
        return new Promise((resolve) => {
            setTimeout(() => resolve(), time);
        });
    }

    function openInTab(url, option) {
        GM_openInTab(url, option);
    }

    function getTmValue(key) {
        return GM_getValue(key);
    }

    function setTmValue(key, value) {
        GM_setValue(key, value);
    }

    function FloatBall(id, option) {
        if (!id) {
            throw new Error("FloatBall:创建悬浮球失败,至少需要指定id。");
        }
        if (document.getElementById(id)) {
            throw new Error("FloatBall:创建悬浮球失败,指定id的元素已存在。");
        }
        if (!option) {
            option = {};
        }
        let defaultOption = {
            color: "red",
            diameter: "40px",
            opacity: "0.5"
        };
        for (let name in defaultOption) {
            if (option[name] === undefined) {
                option[name] = defaultOption[name];
                console.log(`FloatBall:未传入参数${name},将使用默认值${defaultOption[name]}。`);
            }
        }
        console.log("FloatBall:配置完毕");
        //添加默认的css,::-webkit-scrollbar只有id选择器可以用
        //伪类中的highlight设置为透明,解决部分网站点击小球出现蓝色高光的问题
        //类名首字母写两位,防止与被注入的网站冲突
        let cssCode = '.bball{position:fixed;cursor:pointer;border-radius:99px;z-index:999999;}' +
            '.bball:focus,.bball:active,.bball:hover{-webkit-tap-highlight-color:transparent;}';
        addCSS(cssCode);
        //使用div作为小球的元素,一方面div一般不会被加样式,一方面在移动端选词后点击可以直接取消选词,button就不行
        //a标签也由于本身的意义不适合作为小球的元素
        let ball = document.createElement("div");
        ball.id = id;
        ball.className = "bball";
        ball.style.width = option.diameter;
        ball.style.height = option.diameter;
        ball.style.opacity = option.opacity;
        ball.style.backgroundColor = option.color;
        let ballX;
        let ballY;
        //从插件获取悬浮球上次的位置,不存在就使用默认位置
        let lastPosition = getTmValue(ball.id);
        if (lastPosition) {
            let position = lastPosition.split(",");
            //数值异常或越界使用默认位置
            if (position[0] > window.innerWidth || position[1] > window.innerHeight) {
                move(0, 0.5 * window.innerHeight);
            } else move(position[0], position[1]);
        } else {
            move(0, 0.5 * window.innerHeight);
        }
        document.body.prepend(ball);
        //鼠标与小球位置差值
        let _x;
        let _y;
        //不同设备需要区别触发事件
        let events;
        if (isAndroid) {
            events = ["touchstart", "touchmove", "touchend"];
        } else events = ["mousedown", "mousemove", "mouseup"];
        let moveListenerHandler = function (e) {
            e.preventDefault();
            //移动端多指触控处理
            if (e.type === "touchmove") {
                e = e.touches[0];
            }
            console.log("touchmove/mousemove:移动小球。");
            //移动小球时需要保持相对位置,同时处理屏幕左方与上方越界
            move(e.pageX - _x < 0 ? 0 : e.pageX - _x, e.pageY - _y < 0 ? 0 : e.pageY - _y);
        };
        let freeHandler = function () {
            //手指或鼠标放开,清除移动监听器,随后记录位置到插件
            document.removeEventListener(events[1], moveListenerHandler);
            console.log("mouseup/touchend:已清除移动事件监听。");
            setTmValue(ball.id, ballX + "," + ballY);
            //清除自身
            document.removeEventListener(events[2], freeHandler);
            console.log("mouseup/touchend:已清除抬起事件监听。");
        };
        //当小球被按下或触摸开始,添加移动监听器和抬起监视器
        ball.addEventListener(events[0], (e) => {
            //移动端多指触控处理
            if (e.type === "touchstart") {
                e = e.touches[0];
            }
            //记录小球位置与鼠标位置的差值
            _x = e.pageX - ballX;
            _y = e.pageY - ballY;
            document.addEventListener(events[1], moveListenerHandler, {passive: false}/*解决移动端上下移动小球触发屏幕滑动*/);
            console.log("mousedown/touchstart:已添加移动事件监听器。");
            document.addEventListener(events[2], freeHandler);
            console.log("mousedown/touchstart:已添加抬起事件监听器。");
        });

        function move(x, y) {
            //获取直径数值
            let diameter = Number(option.diameter.replace("px", ""));
            //处理屏幕右方与下方越界
            x = x > window.innerWidth - diameter ? window.innerWidth - diameter : x;
            y = y > window.innerHeight - diameter ? window.innerHeight - diameter : y;
            ballX = x;
            ballY = y;
            ball.style.left = x + "px";
            ball.style.top = y + "px";
        }
    }

    function ModalDialog(innerHTML, option) {
        if (!innerHTML) {
            innerHTML = "";
        }
        if (!option) {
            option = {};
        }
        //参数校验
        let defaultOption = {
            dialogPosition: "middle",
            width: "300px",
            backgroundColor: "white",
            backgroundOpacity: "0.8",
            //是否可以通过点击背景关闭模态框
            canBeClose: true
        };
        if (option.dialogPosition && !/top|middle|bottom/.test(option.dialogPosition)) {
            option.dialogPosition = "middle";
            console.log("ModalDialog:位置名称错误,将使用middle位置。");
        }
        for (let name in defaultOption) {
            if (option[name] === undefined) {
                option[name] = defaultOption[name];
                console.log("ModalDialog:未传入配置" + name + ",将使用默认配置" + defaultOption[name] + "。");
            }
        }
        console.log("ModalDialog:配置完毕");
        //添加默认的css,::-webkit-scrollbar不能把_下划线开头的选择器放在前面,会失效
        //类名首字母写两位,防止与被注入的网站冲突
        let cssCode = '.ddialogBackground{position:fixed;inset:0px;visibility:hidden;background:rgba(0,0,0,0);}' +
            '.ddialog{overflow-y:overlay;box-sizing:border-box;margin:auto;border-radius:8px;visibility:hidden;text-align:center;opacity:0;}' +
            '.ddialog::-webkit-scrollbar{display:none;}';
        let transitionCss = '.ddialogBackground{transition:background 0.3s,visibility 0.3s;}' +
            '.ddialog{transition:opacity 0.3s,visibility 0.3s,margin-top 0.3s linear,bottom 0.3s linear;}';
        addCSS(cssCode);
        if (!isAndroid) {
            addCSS(transitionCss);
        }
        //模态框全屏容器,压暗
        let dialogBackground = document.createElement("div");
        dialogBackground.className = "ddialogBackground";
        //添加点击事件,点击背景可以关闭当前模态框
        if (option.canBeClose) {
            dialogBackground.addEventListener("click", (e) => {
                if (e.target !== dialogBackground) {
                    return;
                }
                this.close();
            }, true);
        }
        document.body.prepend(dialogBackground);
        //模态框
        let dialog = document.createElement("div");
        dialog.innerHTML = innerHTML;
        dialog.className = "ddialog";
        dialog.style.maxHeight = window.innerHeight - 90 + "px";
        dialog.style.width = option.width;
        dialog.style.backgroundColor = option.backgroundColor;
        dialogBackground.prepend(dialog);
        if (option.dialogPosition === "top") {
            //移动端考虑到性能,不整动画
            if (isAndroid) {
                dialog.style.marginTop = "60px";
            } else {
                //60-20,给20做动画,动画结束后是60
                dialog.style.marginTop = "40px";
            }
        } else if (option.dialogPosition === "middle") {
            let a, b;
            if (isAndroid) {
                a = 0;
                b = 60;
            } else {
                //-20,打开时会+20
                a = 20;
                b = 40;
            }
            let marginTop = Math.floor(innerHeight * 0.5 - dialog.scrollHeight / 2) - a;
            dialog.style.marginTop = (marginTop < b ? b : marginTop) + 'px';
        } else if (option.dialogPosition === "bottom") {
            dialog.style.position = 'absolute';
            dialog.style.left = '50%';
            dialog.style.marginLeft = '-' + dialog.clientWidth / 2 + 'px';
            if (isAndroid) {
                dialog.style.bottom = '30px';
            } else {
                //动画结束后是30
                dialog.style.bottom = '10px';
            }
        }
        //开关状态
        this.opened = false;
        //定义打开与关闭方法
        this.open = function () {
            //打开时指定z-index,模态框依次叠加
            if (!window.maxDialogZIndex) {
                //7个9,基数
                //并非每次都是回到这个数值,因为有允许先开的先关,此时最大值是不该递减的
                //因为后开那个才是最上层,最终关完,提前关了几个,数值就会大几
                window.maxDialogZIndex = 9999999;
            }
            dialogBackground.style.zIndex = window.maxDialogZIndex + 1;
            window.maxDialogZIndex += 1;
            //显示
            //下面的样式书写书序没有影响
            dialogBackground.style.background = "rgba(0,0,0," + option.backgroundOpacity + ")";
            dialogBackground.style.visibility = "visible";
            dialog.style.opacity = "1";
            dialog.style.visibility = "visible";
            if (!isAndroid) {
                if (option.dialogPosition === "bottom") {
                    dialog.style.bottom = '30px';
                } else {
                    let top = Number(dialog.style.marginTop.replace("px", "")) + 20;
                    dialog.style.marginTop = top + 'px';
                }
            }
            this.opened = true;
        };
        this.close = function () {
            //显示
            dialogBackground.style.visibility = "hidden";
            dialogBackground.style.background = "rgba(0,0,0,0)";
            dialog.style.visibility = "hidden";
            dialog.style.opacity = "0";
            if (!isAndroid) {
                if (option.dialogPosition === "bottom") {
                    dialog.style.bottom = '10px';
                } else {
                    let top = Number(dialog.style.marginTop.replace("px", "")) - 20;
                    dialog.style.marginTop = top + 'px';
                }
            }
            //修改最大z-index
            //需要判断关闭的是不是最上层的再递减全局变量
            if (dialogBackground.style.zIndex === String(window.maxDialogZIndex)) {
                window.maxDialogZIndex -= 1;
            }
            this.opened = false;
        };
    }

    function WindowBling(option) {
        if (!option) {
            option = {};
        }
        let defaultOption = {
            color: "purple",
            blingPosition: ["top", "bottom", "left", "right"],
            blurRadius: "80px",
            spreadRadius: "30px"
        };
        for (let name in defaultOption) {
            if (option[name] === undefined) {
                option[name] = defaultOption[name];
                console.log("WindowBling:未传入参数" + name + ",将使用默认值" + defaultOption[name] + "。");
            }
        }
        console.log("WindowBling:配置完毕");
        let blingArray = new Array(4);
        //opacity,visibility需要改动,写在元素里
        let topAndBottomStyle = "left:0px;right:0px;height:0px;opacity:0;visibility:hidden;";
        let leftAndRightStyle = "top:0px;bottom:0px;width:0px;opacity:0;visibility:hidden;";
        //通用css,使用选择器的方式方便调试
        let cssCode = '.bbling{z-index:999998;position:fixed;transition:opacity 0.5s,visibility 0.5s;';
        cssCode += "box-shadow:" + "0 0 " + option.blurRadius + " " + option.spreadRadius + " " + option.color + ";}";
        addCSS(cssCode);
        //给传入需要bling的位置创建元素并指定各自的样式
        for (let position of option.blingPosition) {
            let bling = document.createElement("div");
            bling.className = "bbling";
            bling.style[position] = "0px";
            if (position === "top" || position === "bottom") {
                bling.style.cssText += topAndBottomStyle;
            } else if (position === "left" || position === "right") {
                bling.style.cssText += leftAndRightStyle;
            }
            document.body.prepend(bling);
            blingArray.push(bling);
        }
        this.blink = function () {
            let handler = function () {
                for (let bling of blingArray) {
                    if (bling) {
                        if (bling.style.opacity === "0") {
                            bling.style.opacity = "1";
                        } else {
                            bling.style.opacity = "0";
                        }
                        if (bling.style.visibility === "hidden") {
                            bling.style.visibility = "visible";
                        } else {
                            bling.style.visibility = "hidden";
                        }
                    }
                }
            };
            //只闪一下
            handler();
            setTimeout(handler, 500);
        };
    }
})();