qqshow 1.4

优化按钮布局与视觉体验,支持拖拽悬浮按钮管理自定义头像

当前为 2025-05-13 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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