Greasy Fork

Greasy Fork is available in English.

MWI QQShow Offline

QQ Show offline.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MWI QQShow Offline
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  QQ Show offline.
// @author       guch8017
// @match        https://www.milkywayidle.com/*
// @match        https://www.milkywayidlecn.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @license      MIT
// ==/UserScript==

/*
 * QQ秀插件-离线版
 * 由Ratatata的Magic Way Idle的代码精简而来,仅保留了QQ秀功能。
 * 该插件不包含联网相关代码,仅对本地游戏账户有效,如需联网功能请考虑使用在线版。
 */


(function() {
    'use strict';

    const hasMagicWayIdle = false;
    const QQSHOW_CLS = {
        qqshow_setting: "qqshow_md3",
        qqshow_url_input: "qqshow_url_input_md3",
        qqshow_key: "qqshow_offline_url",
    };
    const buttonThor = 1000;
    let globalVariable = {
        qqShow:{
            // 保存玩家QQ秀链接
            replacementTargets : {},
            // 图标替换观察者
            observer : null,
            characterName : null
        }
    }
    let lastTimeClick = 0;
    function hookWebSocket() {
        const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
        const oriGet = dataProperty.get;
        dataProperty.get = hookedGet;
        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
        function hookedGet() {
            const socket = this.currentTarget;
            if (!(socket instanceof WebSocket)) {
                return oriGet.call(this);
            }
            if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
                return oriGet.call(this);
            }
            const message = oriGet.call(this);
            Object.defineProperty(this, "data", { value: message });
            return handleMessage(message);
        }
    }
    function handleMessage(message,debug=false) {
        let obj = JSON.parse(message);
        if (obj && obj.type === "init_character_data") {
            // 读取角色名称用于上传QQ秀
            globalVariable.qqShow.characterName=obj.character.name;
        }
        return message;
    }

    // Helper function 显示提醒
    // showToast()
    // Source: **助手
    // Author: Trutn_Light Stella
    const toastQueues = Array.from({ length: 5 }, () => []);
    const maxVisibleToasts = Math.floor(window.innerHeight / 2 / 50);
    let isToastVisible = Array(5).fill(false);
    function displayNextToast(queueIndex) {
        if (isToastVisible[queueIndex] || toastQueues[queueIndex].length === 0) return;
        const { message, duration } = toastQueues[queueIndex].shift();
        isToastVisible[queueIndex] = true;
        const toast = createToastElement(message, queueIndex);
        toast.style.opacity = '0';
        requestAnimationFrame(() => {
            toast.style.opacity = '1';
        });
        setTimeout(() => {
            toast.style.opacity = '0';
            setTimeout(() => {
                document.body.removeChild(toast);
                isToastVisible[queueIndex] = false;
                displayNextToast(queueIndex);
            }, 500);
        }, duration);
    }
    function showToast(message, duration = 2000) {
        const queueIndex = toastQueues.findIndex(queue => queue.length < maxVisibleToasts);
        if (queueIndex === -1) return;
        toastQueues[queueIndex].push({ message, duration });
        displayNextToast(queueIndex);
    }
    function createToastElement(message, queueIndex) {
        const toast = document.createElement('div');
        toast.className = 'toast';
        toast.style.position = 'fixed';
        toast.style.bottom = `${20 + queueIndex * 60}px`;
        toast.style.left = '50%';
        toast.style.transform = 'translateX(-50%)';
        toast.style.backgroundColor = '#333';
        toast.style.color = '#fff';
        toast.style.padding = '10px 20px';
        toast.style.borderRadius = '5px';
        toast.style.zIndex = '1000';
        toast.style.textAlign = 'center';
        toast.style.transition = 'opacity 0.5s';
        toast.textContent = message;
        document.body.appendChild(toast);
        return toast;
    }

    function addQQshowButton() {
        const targetNode = document.querySelector("div.SettingsPanel_infoGrid__2nh1u");
        const isqqshowFlagExist = document.querySelector(`div.${QQSHOW_CLS.qqshow_setting}`);
        if(targetNode&&!isqqshowFlagExist){
            const nameColor=targetNode.querySelectorAll("div.SettingsPanel_value__2nsKD")[2];
            let qqshowtitlediv = document.createElement("div");
            let qqshowdiv = document.createElement("div");
            let qqshowdivflag = document.createElement("div");
            qqshowtitlediv.setAttribute("class", "SettingsPanel_label__24LRD");
            qqshowtitlediv.innerHTML="更新QQ秀【离线版】";
            qqshowdiv.setAttribute("class", "SettingsPanel_value__2nsKD");
            qqshowdiv.style=nameColor.style;
            qqshowdivflag.setAttribute("class", QQSHOW_CLS.qqshow_setting);
            let qqshowURLInput = document.createElement("input");
            qqshowURLInput.type = "text";
            qqshowURLInput.setAttribute("class", QQSHOW_CLS.qqshow_url_input);
            qqshowURLInput.placeholder = "图床url/提交空白视为删除";
            let qqshowSubmitButton = document.createElement("button");
            qqshowSubmitButton.setAttribute("class", "Button_button__1Fe9z");
            qqshowSubmitButton.textContent = "提交";
            qqshowSubmitButton.addEventListener("click", qqshowSubmit);
            qqshowdiv.appendChild(qqshowdivflag);
            qqshowdiv.appendChild(qqshowURLInput);
            qqshowdiv.appendChild(qqshowSubmitButton);
            let readmetitlediv = document.createElement("div");
            let readme = document.createElement("div");
            readmetitlediv.setAttribute("class", "SettingsPanel_label__24LRD");
            readme.setAttribute("class", "SettingsPanel_value__2nsKD");
            readme.innerHTML="先去tupian.li等图床上传图片,再提交url。<br> 直接提交空白将删除QQ秀。刷新后生效。<br> 若依然无效请点击强制刷新缓存后,再次刷新页面。"

            nameColor.parentNode.insertBefore(readme, nameColor.nextSibling);
            nameColor.parentNode.insertBefore(readmetitlediv, nameColor.nextSibling);

            nameColor.parentNode.insertBefore(qqshowdiv, nameColor.nextSibling);
            nameColor.parentNode.insertBefore(qqshowtitlediv, nameColor.nextSibling);
        }
    }

    function qqshowSubmit(){
        const now = Date.now();
        if (now - lastTimeClick < buttonThor) return;
        lastTimeClick = now;
        let qqshowURLInput=document.querySelector(`input.${QQSHOW_CLS.qqshow_url_input}`);
        let url=qqshowURLInput.value
        function isValidURL(str) {
            try {
                new URL(str);
                return true;
            } catch (err) {
                return false;
            }
        }
        if(url==''){
            showToast("已删除,刷新生效");
            updateqqshow(url);
        }else if(isValidURL(url)){
            showToast("已提交,刷新生效");
            updateqqshow(url);
        }else{
            showToast("url不合法");
        }
    }

    //更新QQ秀
     function updateqqshow(face_url){
        if (document.URL.includes("test.milkywayidle.com"))return;
        if (globalVariable.qqShow.characterName == "" || typeof globalVariable.qqShow.characterName === "undefined") {
            showToast("非法更新,请刷新页面");
            return;
        }
        let qqshow_data = localStorage.getItem(QQSHOW_CLS.qqshow_key);
        if (qqshow_data == null) {
            qqshow_data = {};
        } else {
            qqshow_data = JSON.parse(qqshow_data);
        }
        if (face_url == null || face_url == "") {
            delete qqshow_data[globalVariable.qqShow.characterName];
        } else {
            qqshow_data[globalVariable.qqShow.characterName] = face_url;
        }
        localStorage.setItem(QQSHOW_CLS.qqshow_key, JSON.stringify(qqshow_data));
    }

    // Source: MWI玩家图标替换
    // Author: Ak4r1 ChatGpt Stella bot7420
    function replaceIconsIn(node) {
        const iconElements = node.querySelectorAll(`div.FullAvatar_fullAvatar__3RB2h`);
        for (const elem of iconElements) {
            if (elem.closest("div.CowbellStorePanel_avatarsTab__1nnOY")) {
                continue; // 商店页面
            }

            const playerId = findPlayerIdByAvatarElem(elem);
            if (!playerId) {
                //console.error("ICONS: replaceIconsIn can't find playerId");
                //设置页面下面两个小人会引发异常,不要大惊小怪
                //console.log(elem);
                continue; // 找不到 playerId
            }

            if (!globalVariable.qqShow.replacementTargets.hasOwnProperty(playerId)) {
                continue; // 没有配置图片地址
            }

            const newImgElement = document.createElement("img");
            newImgElement.src = globalVariable.qqShow.replacementTargets[playerId];
            newImgElement.style.width = "100%";
            newImgElement.style.height = "100%";
            elem.innerHTML = "";
            elem.appendChild(newImgElement);
        }
    }
    function findPlayerIdByAvatarElem(avatarElem) {
        // Profile 窗口页
        const profilePageDiv = avatarElem.closest("div.SharableProfile_modal__2OmCQ");
        if (profilePageDiv) {
            return profilePageDiv.querySelector(".CharacterName_name__1amXp")?.textContent.trim();
        }
        // 网页右上角
        const headerDiv = avatarElem.closest("div.Header_header__1DxsV");
        if (headerDiv) {
            return headerDiv.querySelector(".CharacterName_name__1amXp")?.textContent.trim();
        }
        // 战斗页面
        const combatDiv = avatarElem.closest("div.CombatUnit_combatUnit__1m3XT");
        if (combatDiv) {
            return combatDiv.querySelector(".CombatUnit_name__1SlO1")?.textContent.trim();
        }

        // 组队页面
        const partyDiv = avatarElem.closest("div.Party_partySlot__1xuiq");
        if (partyDiv) {
            return partyDiv.querySelector(".CharacterName_name__1amXp")?.textContent.trim();
        }
        return null;
    }

    //初始化观察者,分配替换目标
    function initQQShowObserver(){
        globalVariable.qqShow.observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (
                        node.tagName === "DIV" &&
                        !node.classList.contains("ProgressBar_innerBar__3Z_sf") &&
                        !node.classList.contains("CountdownOverlay_countdownOverlay__2QRmL") &&
                        !node.classList.contains("ChatMessage_chatMessage__2wev4") &&
                        !node.classList.contains("Header_loot__18Cbe") &&
                        !node.classList.contains("script_itemLevel") &&
                        !node.classList.contains("script_key") &&
                        !node.classList.contains("dps-info") &&
                        !node.classList.contains("MuiTooltip-popper")
                    ) {
                        replaceIconsIn(node);
                    }
                });
            });
        });
    }

    function gameMain(){
        // 拦截WebSocket
        hookWebSocket();
        // 优先从缓存加载QQ秀
        if(QQSHOW_CLS.qqshow_key in localStorage){
            globalVariable.qqShow.replacementTargets=JSON.parse(localStorage.getItem(QQSHOW_CLS.qqshow_key));
        }
        // 初始化观察者,分配替换目标
        initQQShowObserver();
        // 启动观察者,替换QQ秀
        globalVariable.qqShow.observer.observe(document, { attributes: false, childList: true, subtree: true });

        // 设置页面仍然需要添加新的图标 初始化设置页面观察者
        let globalObserver=new MutationObserver(function (mutationsList, observer) {
            addQQshowButton();
        });
        globalObserver.observe(document,{ childList: true, subtree: true });
    }

    gameMain()
})();