// ==UserScript== // @name qqshow 1.4 // @namespace http://tampermonkey.net/ // @version 1.4 // @description 优化按钮布局与视觉体验,支持拖拽悬浮按钮管理自定义头像 // @author VoltaX (Modified by whosyourdaddy) // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @connect https://qqshow.131.996h.cn // @connect https://mwi-avatar.voltax.workers.dev // @icon http://milkywayidle.com/favicon.ico // @license MIT // @grant none // @downloadURL none // ==/UserScript== //样式 const css = ` .custom-mwi-avatar { width: 100%; height: 100%; } .floating-btn { position: fixed; width: 50px; height: 50px; border-radius: 50%; background: transparent; display: flex; justify-content: center; align-items: center; cursor: move; z-index: 99999; font-size: 30px; text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease; right: 20px; bottom: 20px; } .floating-btn.dragging { transform: scale(1.05); } .floating-panel { position: fixed; background: white; padding: 20px 25px; border-radius: 15px; box-shadow: 0 6px 25px rgba(0, 0, 0, 0.1); min-width: 350px; display: none; z-index: 99998; font-family: Arial, sans-serif; text-align: center; } .floating-panel.active { display: block; } .input-group { margin-bottom: 20px; } .input-group input { width: 100%; padding: 10px 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; } .button-group { display: flex; justify-content: center; gap: 15px; margin-top: 25px; } .action-btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; transition: background-color 0.3s, transform 0.2s; } .action-btn-primary { background-color: #3498db; color: white; box-shadow: 0 2px 5px rgba(52, 152, 219, 0.2); } .action-btn-secondary { background-color: #f5f5f5; color: #333; border: 1px solid #e0e0e0; } .action-btn:hover { transform: scale(1.02); } .action-btn-primary:hover { background-color: #2980b9; } .action-btn-secondary:hover { background-color: #f0f0f0; } .error-message { color: #e74c3c; font-size: 0.9em; margin-top: 15px; text-align: center; } `; const InsertStyleSheet = (style) => { const s = new CSSStyleSheet(); s.replaceSync(style); document.adoptedStyleSheets = [...document.adoptedStyleSheets, s]; }; InsertStyleSheet(css); //工具函数 const HTML = (tagname, attrs, ...children) => { if (attrs === undefined) return document.createTextNode(tagname); const ele = document.createElement(tagname); for (const [key, value] of Object.entries(attrs)) { if (value === null || value === undefined) continue; key.startsWith('_') ? ele.addEventListener(key.slice(1), value) : ele.setAttribute(key, value); } children.forEach(child => child && ele.append(child)); return ele; }; //核心逻辑 const RemoteHost1 = "https://qqshow.131.996h.cn"; const AvatarPath1 = "/get-avatar.php"; const AvatarsPath1 = "/get-avatars.php"; const UploadPath1 = "/set-avatar.php"; const RemoteHost2 = "https://mwi-avatar.voltax.workers.dev"; const AvatarPath2 = "/get-avatar"; const AvatarsPath2 = "/get-avatars"; const UploadPath2 = "/set-avatar"; let PlayerUsername = ""; let avatarCache1; let lastUpdated1; let avatarCache2; let lastUpdated2; const expireTime = 3 * 60 * 60 * 1000; class Lock { #queue = []; #count = 0; constructor(count) { this.#count = count; this.release = this.release.bind(this); } acquire() { if (this.#count > 0) { this.#count -= 1; return this.release; } else { const { promise, resolve } = Promise.withResolvers(); this.#queue.push(resolve); return promise; } } release() { if (this.#queue.length > 0) { const front = this.#queue.shift(); front(this.release); } else this.#count += 1; } } const ReqLock1 = new Lock(1); const ReqLock2 = new Lock(1); const InitCache1 = () => { try { avatarCache1 = JSON.parse(window.localStorage.getItem("custom-avatar-cache1") ?? "undefined"); lastUpdated1 = JSON.parse(window.localStorage.getItem("custom-avatar-cache-updated1") ?? "undefined"); } catch (e) { avatarCache1 = undefined; lastUpdated1 = undefined; } }; InitCache1(); const SaveCache1 = () => { window.localStorage.setItem("custom-avatar-cache1", JSON.stringify(avatarCache1)); window.localStorage.setItem("custom-avatar-cache-updated1", JSON.stringify(lastUpdated1)); }; const UpdateCache1 = async () => { const res = await fetch(`${RemoteHost1}${AvatarsPath1}`, { mode: "cors" }); if (res.status === 200) { avatarCache1 = await res.json(); lastUpdated1 = new Date().getTime(); SaveCache1(); return true; } else return false; }; const CheckCache1 = async (username) => { if (!lastUpdated1 || !avatarCache1 || (new Date().getTime() - lastUpdated1 >= expireTime)) { const cacheValid = await UpdateCache1(); if (cacheValid) return avatarCache1[username]; else return false; } else return avatarCache1[username]; }; const GetCustomAvatar1 = async (username) => { const lock = await ReqLock1.acquire(); const result = await CheckCache1(username); lock(); return result; }; const InitCache2 = () => { try { avatarCache2 = JSON.parse(window.localStorage.getItem("custom-avatar-cache2") ?? "undefined"); lastUpdated2 = JSON.parse(window.localStorage.getItem("custom-avatar-cache-updated2") ?? "undefined"); } catch (e) { avatarCache2 = undefined; lastUpdated2 = undefined; } }; InitCache2(); const SaveCache2 = () => { window.localStorage.setItem("custom-avatar-cache2", JSON.stringify(avatarCache2)); window.localStorage.setItem("custom-avatar-cache-updated2", JSON.stringify(lastUpdated2)); }; const UpdateCache2 = async () => { const res = await fetch(`${RemoteHost2}${AvatarsPath2}`, { mode: "cors" }); if (res.status === 200) { avatarCache2 = await res.json(); lastUpdated2 = new Date().getTime(); SaveCache2(); return true; } else return false; }; const CheckCache2 = async (username) => { if (!lastUpdated2 || !avatarCache2 || (new Date().getTime() - lastUpdated2 >= expireTime)) { const cacheValid = await UpdateCache2(); if (cacheValid) return avatarCache2[username]; else return false; } else return avatarCache2[username]; }; const GetCustomAvatar2 = async (username) => { const lock = await ReqLock2.acquire(); const result = await CheckCache2(username); lock(); return result; }; const GetCustomAvatar = async (username) => { const avatar1 = await GetCustomAvatar1(username); if (avatar1) return avatar1; const avatar2 = await GetCustomAvatar2(username); return avatar2; }; const ReplaceHeaderAvatar = async () => { const characterInfoDiv = document.querySelector("div.Header_characterInfo__3ixY8:not([avatar-modified])"); if (!characterInfoDiv) return; characterInfoDiv.setAttribute("avatar-modified", ""); const username = characterInfoDiv.querySelector(":scope div.CharacterName_name__1amXp").dataset.name; if (!PlayerUsername) PlayerUsername = username; const avatarWrapperDiv = characterInfoDiv.querySelector(":scope div.Header_avatar__2RQgo"); const avatarURL = await GetCustomAvatar(username); if (avatarURL) { avatarWrapperDiv.replaceChildren( HTML("img", { class: "custom-mwi-avatar", src: avatarURL }) ); } }; const ReplaceProfileAvatar = async () => { const profileDiv = document.querySelector("div.SharableProfile_modal__2OmCQ:not([avatar-modified])"); if (!profileDiv) return; profileDiv.setAttribute("avatar-modified", ""); const username = profileDiv.querySelector(":scope div.CharacterName_name__1amXp").dataset.name; const avatarWrapperDiv = profileDiv.querySelector(":scope div.FullAvatar_fullAvatar__3RB2h.FullAvatar_xlarge__1cmUN"); const avatarURL = await GetCustomAvatar(username); if (avatarURL) { avatarWrapperDiv.replaceChildren( HTML("img", { class: "custom-mwi-avatar", src: avatarURL }) ); } }; const ReplacePartyMember = async () => { const slotDiv = document.querySelector("div.Party_partySlots__3zGeH:not([avatar-modified])"); if (!slotDiv) return; slotDiv.setAttribute("avatar-modified", ""); const username = slotDiv.querySelector(":scope div.CharacterName_name__1amXp").dataset.name; const avatarWrapperDiv = slotDiv.querySelector(":scope div.FullAvatar_fullAvatar__3RB2h.FullAvatar_large__fJGwX"); const avatarURL = await GetCustomAvatar(username); if (avatarURL) { avatarWrapperDiv.replaceChildren( HTML("img", { class: "custom-mwi-avatar", src: avatarURL }) ); } }; const ReplaceCombatUnit = async () => { const unitDiv = document.querySelector("div.CombatUnit_combatUnit__1m3XT:not([avatar-modified])"); if (!unitDiv) return; unitDiv.setAttribute("avatar-modified", ""); const username = unitDiv.querySelector(":scope div.CombatUnit_name__1SlO1").textContent; const avatarWrapperDiv = unitDiv.querySelector(":scope div.FullAvatar_fullAvatar__3RB2h"); const avatarURL = await GetCustomAvatar(username); if (avatarURL) { avatarWrapperDiv.replaceChildren( HTML("img", { class: "custom-mwi-avatar", src: avatarURL }) ); } }; const UploadAvatar = async () => { const URLInput = document.getElementById("custom-avatar-url-input").value; const errorSpan = document.getElementById("custom-avatar-upload-error"); try { const toURL = new URL(URLInput); if (toURL.protocol !== "https:") { errorSpan.textContent = "输入的链接协议不是https"; return; } } catch (e) { if (e instanceof TypeError) { errorSpan.textContent = "输入的链接不是有效的URL"; return; } else console.error(e); } errorSpan.textContent = "上传中请点击刷新缓存"; // 先尝试上传到第一个主机 const res1 = await fetch(`${RemoteHost1}${UploadPath1}`, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: PlayerUsername, imageURL: URLInput, }) }); if (res1.status === 200) { if (!avatarCache1) avatarCache1 = {}; avatarCache1[PlayerUsername] = URLInput; SaveCache1(); } else { errorSpan.textContent += ` 尝试主机1失败:${res1.status} ${await res1.text()}`; } // 再尝试上传到第二个主机 const res2 = await fetch(`${RemoteHost2}${UploadPath2}`, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: PlayerUsername, imageURL: URLInput, }) }); if (res2.status === 200) { if (!avatarCache2) avatarCache2 = {}; avatarCache2[PlayerUsername] = URLInput; SaveCache2(); errorSpan.textContent = "成功上传"; RefreshAvatar(); } else { errorSpan.textContent += ` 尝试主机2失败:${res2.status} ${await res2.text()}`; } }; const ManualRefresh = async () => { const errorSpan = document.getElementById("custom-avatar-upload-error"); avatarCache1 = undefined; lastUpdated1 = undefined; avatarCache2 = undefined; lastUpdated2 = undefined; errorSpan.textContent = "准备刷新"; try { await GetCustomAvatar(PlayerUsername); } catch (e) { errorSpan.textContent = "刷新时出现错误,请不要联系我"; } errorSpan.textContent = "刷新完成"; RefreshAvatar(); }; const ShowHelp = () => { const errorSpan = document.getElementById("custom-avatar-upload-error"); errorSpan.textContent = "上传HTTPS的图片链接图片将被设置为牛牛头像"; }; // ------------------------ 悬浮UI创建(修改按钮组布局) ------------------------ let floatingBtn, floatingPanel; let isDragging = false; let startX, startY, startLeft, startTop; const CreateFloatingUI = () => { floatingBtn = document.createElement('div'); floatingBtn.className = 'floating-btn'; floatingBtn.textContent = '🐄'; floatingPanel = document.createElement('div'); floatingPanel.className = 'floating-panel'; floatingPanel.innerHTML = `