MWI QQShow Offline

QQ Show offline.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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()
})();