// ==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();
});