osu! mapper highlight

highlights mapsets of favourite mappers

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         osu! mapper highlight
// @namespace    https://osu.ppy.sh/users/10767001
// @version      2026-08-06
// @description  highlights mapsets of favourite mappers
// @author       Hagama
// @match        https://osu.ppy.sh/*
// @match        http://osu.ppy.sh/*
// @icon         https://osu.ppy.sh/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    console.log('Tampermonkey osu-mapper-highlight loaded');
    const STORAGE_KEY = 'tm-mapper-list';
    const id = 'tm-mapper-highlight';

    if (document.getElementById(id)) return;

    const defaultMappers = [1];

    function loadMappers() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (!raw) return [...defaultMappers];
            const parsed = JSON.parse(raw);
            if (!Array.isArray(parsed)) return [...defaultMappers];
            return parsed;
        } catch {
            return [...defaultMappers];
        }
    }
    function saveMappers(arr) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(arr));
        } catch {}
    }

    let userIds = loadMappers();
    console.log(userIds);
    function panelOverride(root = document) {
        const aSel = userIds.map(id => `a[data-user-id="${CSS.escape(String(id))}"]`).join(',');

        root.querySelectorAll('div.beatmapset-panel').forEach(div => {
            div.style.outline = div.querySelector(aSel) ? '2px solid #2ecc71' : '';
        });
    }

    function userpageLoaded() {
        const container = document.querySelector(".profile-detail-bar");
        if (!container) return;
        const divs = container.querySelectorAll("div");
        const secondDiv = divs[1];
        if (!secondDiv) return;
        if (container.querySelector(".tm-add-mapper-button")) return;
        const newDiv = document.createElement("div");
        newDiv.className = "tm-add-mapper-button";
        newDiv.appendChild(mapperBtn);
        secondDiv.insertAdjacentElement("afterend", newDiv);
    }

    const mapperBtn = document.createElement('button');
    mapperBtn.type = 'button';
    mapperBtn.classList.add("user-action-button");
    mapperBtn.classList.add("user-action-button--profile-page");
    mapperBtn.style.width = '100px';
    mapperBtn.textContent = 'HIGHLIGHT';


    function pageUrl() {
        return window.location.href;
    }

    function onUrlChange() {
        const url = pageUrl();
        const urlOK = url.indexOf('ppy.sh/users/');
        const urlStuff = url.split('ppy.sh/users/')[1] ?? '';
        const slashPos = urlStuff.indexOf('/');
        const mapperId = (slashPos === -1) ? urlStuff : urlStuff.slice(0, slashPos);
        (userIds.includes(mapperId)) ? mapperBtn.classList.add('user-action-button--friend') : mapperBtn.classList.remove('user-action-button--friend');
        const idx = userIds.indexOf(mapperId);
        if (urlOK === -1) {
            mapperBtn.disabled = true;
            mapperBtn.style.cursor = 'not-allowed';
            mapperBtn.style.background = 'rgba(0,0,0,0.4)';
            mapperBtn.style.color = '#6A6A6A';
        } else {
            mapperBtn.disabled = false;
            mapperBtn.style.cursor = 'pointer';
            mapperBtn.style.background = '';
            mapperBtn.style.color = '#fff';
        }
    }

    mapperBtn.addEventListener('click', () => {
        const url = pageUrl();
        const urlStuff = url.split('ppy.sh/users/')[1] ?? '';
        const slashPos = urlStuff.indexOf('/');
        const mapperId = (slashPos === -1) ? urlStuff : urlStuff.slice(0, slashPos);
        const idx = userIds.indexOf(mapperId);

        if (idx !== -1) userIds.splice(idx, 1);
        else userIds.push(mapperId);

        saveMappers(userIds);
        panelOverride();
        onUrlChange();
    });

    if ("hidden" in document){
        document.addEventListener("visibilitychange", () => {
            if (document.visibilityState === 'visible') {
                userIds = loadMappers();
                onUrlChange();
            }
        });
    }

    (function () {
        const origPush = history.pushState;
        history.pushState = function (...args) {
            const ret = origPush.apply(this, args);
            onUrlChange();
            return ret;
        };

        const origReplace = history.replaceState;
        history.replaceState = function (...args) {
            const ret = origReplace.apply(this, args);
            onUrlChange();
            return ret;
        };
        window.addEventListener('popstate', onUrlChange);
        window.addEventListener('hashchange', onUrlChange);

        onUrlChange();
    })();
    const obs = new MutationObserver(() => {panelOverride();userpageLoaded();});
    obs.observe(document.documentElement, { childList: true, subtree: true });
})();