Greasy Fork

Greasy Fork is available in English.

Customize and Share MWI Avatar

Allow you to replace your avatar with any image, and share it with other players who also installed this script.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Customize and Share MWI Avatar
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Allow you to replace your avatar with any image, and share it with other players who also installed this script.
// @author       VoltaX
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @connect      https://mwi-avatar.voltax.workers.dev
// @icon         http://milkywayidle.com/favicon.ico
// @grant        none
// ==/UserScript==
const css = 
`
.custom-mwi-avatar{
    width: 100%;
    height: 100%;
}
`;
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);
    if(attrs) for(const [key, value] of Object.entries(attrs)){
        if(value === null || value === undefined) continue;
        if(key.charAt(0) === "_"){
            const type = key.slice(1);
            ele.addEventListener(type, value);
        }
        else if(key === "eventListener"){
            for(const listener of value){
                ele.addEventListener(listener.type, listener.listener, listener.options);
            }
        }
        else ele.setAttribute(key, value);
    }
    for(const child of children) if(child) ele.append(child);
    return ele;
};
const RemoteHost = "https://mwi-avatar.voltax.workers.dev";
const AvatarPath = "/get-avatar";
const AvatarsPath = "/get-avatars";
const UploadPath = "/set-avatar";
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;
    console.log("ReplaceHeader");
    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";
        }
    }
    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 = "成功上传";
        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 = "刷新时出现错误,请联系VoltaX";
    }
    errorSpan.textContent = "刷新完成";
    RefreshAvatar();
};
const ShowHelp = () => {
    const errorSpan = document.getElementById("custom-avatar-upload-error");
    errorSpan.textContent = "帮助信息正在施工中";
};
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", class: "SettingsPanel_value__2nsKD Input_input__2-t98", placeholder: "输入自定义头像的图床链接"}),
            HTML("button", {class: "Button_button__1Fe9z", _click: 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"}),
        ),
    );
    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);
};