// ==UserScript==
// @name MWI QQShow Offline
// @namespace http://tampermonkey.net/
// @version 1.0
// @description QQ Show offline.
// @author guch8017
// @match https://www.milkywayidle.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/536405/MWI%20QQShow%20Offline.user.js
// @updateURL https://update.greasyfork.icu/scripts/536405/MWI%20QQShow%20Offline.meta.js
// ==/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。
直接提交空白将删除QQ秀。刷新后生效。
若依然无效请点击强制刷新缓存后,再次刷新页面。"
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()
})();