您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
优化按钮布局与视觉体验,支持拖拽悬浮按钮管理自定义头像
当前为
// ==UserScript== // @name qqshow(优化版) // @namespace http://tampermonkey.net/ // @version 1.3 // @description 优化按钮布局与视觉体验,支持拖拽悬浮按钮管理自定义头像 // @author whosyourdaddy // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @connect https://qqshow.131.996h.cn // @icon http://milkywayidle.com/favicon.ico // @license MIT // @grant 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 RemoteHost = "https://qqshow.131.996h.cn"; const AvatarPath = "/get-avatar.php"; const AvatarsPath = "/get-avatars.php"; const UploadPath = "/set-avatar.php"; let PlayerUsername = ""; let avatarCache; let lastUpdated; 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 ReqLock = new Lock(1); const InitCache = () => { try { avatarCache = JSON.parse(window.localStorage.getItem("custom-avatar-cache") ?? "undefined"); lastUpdated = JSON.parse(window.localStorage.getItem("custom-avatar-cache-updated") ?? "undefined"); } catch (e) { avatarCache = undefined; lastUpdated = undefined; } }; InitCache(); const SaveCache = () => { window.localStorage.setItem("custom-avatar-cache", JSON.stringify(avatarCache)); window.localStorage.setItem("custom-avatar-cache-updated", JSON.stringify(lastUpdated)); }; const UpdateCache = async () => { const res = await fetch(`${RemoteHost}${AvatarsPath}`, { mode: "cors" }); if (res.status === 200) { avatarCache = await res.json(); lastUpdated = new Date().getTime(); SaveCache(); return true; } else return false; }; const CheckCache = async (username) => { if (!lastUpdated || !avatarCache || (new Date().getTime() - lastUpdated >= expireTime)) { const cacheValid = await UpdateCache(); if (cacheValid) return avatarCache[username]; else return false; } else return avatarCache[username]; }; const GetCustomAvatar = async (username) => { const lock = await ReqLock.acquire(); const result = await CheckCache(username); lock(); return result; }; 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 res = await fetch(`${RemoteHost}${UploadPath}`, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: PlayerUsername, imageURL: URLInput, }) }); if (res.status === 200) { errorSpan.textContent = "成功上传"; if (!avatarCache) avatarCache = {}; avatarCache[PlayerUsername] = URLInput; SaveCache(); RefreshAvatar(); } else errorSpan.textContent = `上传失败:${res.status} ${await res.text()}`; }; const ManualRefresh = async () => { const errorSpan = document.getElementById("custom-avatar-upload-error"); avatarCache = undefined; lastUpdated = 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 = ` <div class="input-group"> <input type="url" id="custom-avatar-url-input" placeholder="输入HTTPS图片链接(如 https://example.com/avatar.png)"> </div> <div class="button-group"> <button id="upload-btn" class="action-btn action-btn-primary">上传头像</button> <button id="help-btn" class="action-btn action-btn-secondary">帮助</button> <button id="refresh-btn" class="action-btn action-btn-secondary">刷新缓存</button> </div> <div id="custom-avatar-upload-error" class="error-message"></div> `; document.body.appendChild(floatingBtn); document.body.appendChild(floatingPanel); floatingBtn.addEventListener('click', () => { floatingPanel.classList.toggle('active'); UpdatePanelPosition(); }); document.getElementById('upload-btn').addEventListener('click', UploadAvatar); document.getElementById('help-btn').addEventListener('click', ShowHelp); document.getElementById('refresh-btn').addEventListener('click', ManualRefresh); // 拖拽功能 floatingBtn.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = floatingBtn.offsetLeft; startTop = floatingBtn.offsetTop; floatingBtn.classList.add('dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const newX = startLeft + (e.clientX - startX); const newY = startTop + (e.clientY - startY); const maxX = window.innerWidth - floatingBtn.offsetWidth; const maxY = window.innerHeight - floatingBtn.offsetHeight; floatingBtn.style.left = `${Math.max(0, Math.min(newX, maxX))}px`; floatingBtn.style.top = `${Math.max(0, Math.min(newY, maxY))}px`; UpdatePanelPosition(); }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; floatingBtn.classList.remove('dragging'); localStorage.setItem('floatingBtnPosition', JSON.stringify({ left: floatingBtn.style.left, top: floatingBtn.style.top })); } }); // 加载位置 const savedPosition = localStorage.getItem('floatingBtnPosition'); if (savedPosition) { const { left, top } = JSON.parse(savedPosition); floatingBtn.style.left = left; floatingBtn.style.top = top; } else { floatingBtn.style.right = '20px'; floatingBtn.style.bottom = '20px'; floatingBtn.style.left = 'auto'; floatingBtn.style.top = 'auto'; } }; const UpdatePanelPosition = () => { if (!floatingPanel.classList.contains('active')) { return; } const btnRect = floatingBtn.getBoundingClientRect(); let left = btnRect.right + 10; let top = btnRect.top; if (left + 350 > window.innerWidth) { left = btnRect.left - 360; } if (top + floatingPanel.offsetHeight > window.innerHeight) { top = window.innerHeight - floatingPanel.offsetHeight - 20; } floatingPanel.style.left = `${left}px`; floatingPanel.style.top = `${top}px`; }; // ------------------------ 其余代码(完全保留) ------------------------ const AddUploadInput = () => { const settingDiv = document.querySelector("div.SettingsPanel_profileTab__214Bj:not([avatar-upload-added])"); if (!settingDiv) return; settingDiv.setAttribute("avatar-upload-added", ""); const settingGrid = settingDiv.children[0]; const frag = document.createDocumentFragment(); frag.append( HTML("div", { class: "SettingsPanel_label__24LRD" }, "上传自定义头像"), HTML("div", { class: "SettingsPanel_value__2nsKD" }, HTML("input", { id: "custom-avatar-url-input-settings", class: "SettingsPanel_value__2nsKD Input_input__2-t98", placeholder: "输入自定义头像的图床链接" }), HTML("button", { class: "Button_button__1Fe9z", _click: () => { const input = document.getElementById("custom-avatar-url-input-settings"); document.getElementById("custom-avatar-url-input").value = input.value; UploadAvatar(); } }, "上传"), HTML("button", { class: "Button_button__1Fe9z", _click: ShowHelp }, "帮助"), HTML("button", { class: "Button_button__1Fe9z", _click: ManualRefresh }, "刷新本地缓存"), ), HTML("div", { class: "SettingsPanel_label__24LRD" }), HTML("div", { class: "SettingsPanel_value__2nsKD" }, HTML("span", { id: "custom-avatar-upload-error-settings" }), ), ); settingGrid.insertBefore(frag, settingGrid.children[0]); }; const OnMutate = (mutlist, observer) => { observer.disconnect(); ReplaceHeaderAvatar(); ReplaceProfileAvatar(); ReplaceCombatUnit(); ReplacePartyMember(); AddUploadInput(); observer.observe(document, { subtree: true, childList: true }); }; const observer = new MutationObserver(OnMutate); observer.observe(document, { subtree: true, childList: true }); const RefreshAvatar = () => { document.querySelectorAll("[avatar-modified]").forEach(ele => ele.removeAttribute("avatar-modified")); OnMutate([], observer); }; window.addEventListener('load', () => { CreateFloatingUI(); });