Romeo Additions

Allows to hide users, display their information on tiles, and enhances the Radar.

Ekde 2021/05/16. Vidu La ĝisdata versio.

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        2.0
// @description    Allows to hide users, display their information on tiles, and enhances the Radar.
// @description:de Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.
// @author         -Ray-, Djamana
// @include        *://*.planetromeo.com/*
// @grant          GM_addStyle
// @require        https://code.jquery.com/jquery-3.5.1.slim.min.js
// ==/UserScript==

// ==== Dependencies ====

/*! waitForKeyElements | https://gist.github.com/BrockA/2625891 */
/*--- waitForKeyElements():  A utility function, for Greasemonkey scripts,
    that detects and handles AJAXed content.
    Usage example:
        waitForKeyElements (
            "div.comments"
            , commentCallbackFunction
        );
        //--- Page-specific function to do what we want when the node is found.
        function commentCallbackFunction (jNode) {
            jNode.text ("This comment changed by waitForKeyElements().");
        }
    IMPORTANT: This function requires your script to have loaded jQuery.
*/
function waitForKeyElements(
    selectorTxt,    /* Required: The jQuery selector string that
                        specifies the desired element(s).
                    */
    actionFunction, /* Required: The code to run when elements are
                        found. It is passed a jNode to the matched
                        element.
                    */
    bWaitOnce,      /* Optional: If false, will continue to scan for
                        new elements even after the first match is
                        found.
                    */
    iframeSelector  /* Optional: If set, identifies the iframe to
                        search.
                    */
) {
    var targetNodes, btargetsFound;

    if (typeof iframeSelector == "undefined")
        targetNodes = $(selectorTxt);
    else
        targetNodes = $(iframeSelector).contents()
            .find(selectorTxt);

    if (targetNodes && targetNodes.length > 0) {
        btargetsFound = true;
        /*--- Found target node(s).  Go through each and act if they
            are new.
        */
        targetNodes.each(function () {
            var jThis = $(this);
            var alreadyFound = jThis.data('alreadyFound') || false;

            if (!alreadyFound) {
                //--- Call the payload function.
                var cancelFound = actionFunction(jThis);
                if (cancelFound)
                    btargetsFound = false;
                else
                    jThis.data('alreadyFound', true);
            }
        });
    }
    else {
        btargetsFound = false;
    }

    //--- Get the timer-control variable for this selector.
    var controlObj = waitForKeyElements.controlObj || {};
    var controlKey = selectorTxt.replace(/[^\w]/g, "_");
    var timeControl = controlObj[controlKey];

    //--- Now set or clear the timer as appropriate.
    if (btargetsFound && bWaitOnce && timeControl) {
        //--- The only condition where we need to clear the timer.
        clearInterval(timeControl);
        delete controlObj[controlKey]
    }
    else {
        //--- Set a timer, if needed.
        if (!timeControl) {
            timeControl = setInterval(function () {
                waitForKeyElements(selectorTxt,
                    actionFunction,
                    bWaitOnce,
                    iframeSelector
                );
            },
                300
            );
            controlObj[controlKey] = timeControl;
        }
    }
    waitForKeyElements.controlObj = controlObj;
}

// ==== CSS ====

GM_addStyle(`
#visits > .layer__container--wider { width:unset; max-width:1227px; }
div[class*='tile--loading--'] .tile__image { background-image:url(/assets/05c2dc53b86dcd7abdb1d8a50346876b.svg); }

.tile__bar { position:absolute; bottom:0; right:0; visibility:hidden; }
.tile__bar_action { background:rgba(0,0,0,0.4); backdrop-filter: blur(3px); display: inline-block; color:white; margin-left: 1px; padding: 0.25rem 0.5rem; }
.tile__bar_action:hover { background-color:#00A3E4; }
.tile__bar_action:active { background-color:#06648B; }
.tile__link:hover .tile__bar { visibility:visible; }
`);

// ==== Script ====

(function () {
    'use strict';
    proxyXhr();
})();

// ---- Language ----

const _strings = {
    "display": {
        "de": "Anzeige",
        "en": "Display"
    },
    "enhancedTiles": {
        "de": "Erweiterte Kacheln",
        "en": "Enhanced tiles"
    },
    "enhancedTilesDesc": {
        "de": "Zeige alle Benutzerdetails auf den Kacheln. Im Radar wird dies Benutzer als 'Plus'-Abonnenten mit großen Kacheln darstellen.",
        "en": "Shows all user details on tiles. The radar will claim users to be 'Plus' subscribers in large tiles."
    },
    "extensionTitle": {
        "en": "Romeo Additions"
    },
    "hiddenUsers": {
        "de": "Ausgeblendete Benutzer",
        "en": "Hidden users"
    },
    "hideUser": {
        "de": "Benutzer ausblenden",
        "en": "Hide user"
    },
    "maxAge": {
        "de": "Maximales Alter",
        "en": "Maximal age"
    },
    "minAge": {
        "de": "Minimales Alter",
        "en": "Minimal age"
    },
    "viewFullImage": {
        "de": "Bild vergrößern",
        "en": "View full image"
    },
}

function getString(key) {
    const lang = document.documentElement.getAttribute("lang") || "en";
    const translations = _strings[key];
    if (translations)
        return translations[lang] || translations["en"] || "%" + key + "%";
    return "%" + key + "%";
}

// ---- Settings ----

const settingNs = "RA_SETTINGS:";

function getEnhancedTiles() {
    return localStorage.getItem(settingNs + "enhancedTiles") == "true";
}

function getHiddenMaxAge() {
    return parseInt(localStorage.getItem(settingNs + "hiddenMaxAge")) || 99;
}

function getHiddenMinAge() {
    return parseInt(localStorage.getItem(settingNs + "hiddenMinAge")) || 18;
}

function getHiddenUsers() {
    return JSON.parse(localStorage.getItem(settingNs + "hiddenUsers")) || [];
}

function setEnhancedTiles(value) {
    localStorage.setItem(settingNs + "enhancedTiles", value);
}

function setHiddenMaxAge(value) {
    localStorage.setItem(settingNs + "hiddenMaxAge", value);
}

function setHiddenMinAge(value) {
    localStorage.setItem(settingNs + "hiddenMinAge", value);
}

function setUserHidden(username, hide) {
    let hiddenUsers = getHiddenUsers();
    if (hide) {
        if (hiddenUsers.length < hiddenUsers.push(username)) {
            hiddenUsers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
            localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(hiddenUsers));
        }
    } else {
        const prevLength = hiddenUsers.length;
        hiddenUsers = hiddenUsers.filter(e => e != username);
        if (prevLength > hiddenUsers.length)
            localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(hiddenUsers));
    }
}

// ---- XHR ----

function isApiRequest(url, verb) {
    // Request must start with "/api/v?/" or "/api/+/" followed by the given verb.
    const matches = url.match(/\/api\/(v[0-9]|\+)\//);
    if (matches && matches.length)
        return url.startsWith(verb, matches[0].length);
    return false;
}

function filterUser(user, hiddenMaxAge, hiddenMinAge, hiddenNames) {
    return user.personal.age >= hiddenMinAge
        && user.personal.age <= hiddenMaxAge
        && !hiddenNames.includes(user.name)
}

function proxyXhr() {
    // Intercept XHR queries and replies by hooking the XHR open method.
    const realOpen = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
        this.addEventListener("load", () => {
            console.log("[RA] XHR reply: method=" + method + ", url=" + url);
            try {
                // Parse data.
                const isString = typeof this.response === "string";
                reply = isString ? JSON.parse(this.response) : this.response;

                // Modify interesting data.
                if (isApiRequest(url, "notifications")) {
                    reply = xhrHideNotifications(reply);
                }
                if (isApiRequest(url, "profiles")) {
                    reply = xhrEnhanceUsers(reply);
                }
                if (isApiRequest(url, "visitors") || isApiRequest(url, "visits")) {
                    reply = xhrEnhanceUsers(reply);
                    reply = xhrRestorePlusVisit(reply);
                }

                // Write back possibly modified data.
                Object.defineProperty(this, "responseText", { writable: true });
                this.responseText = isString ? JSON.stringify(reply) : reply;
            } catch (e) {
                console.log("[RA] XHR handler failed: " + e)
            }
        });

        // Forward to client.
        return realOpen.apply(this, arguments);
    }
}

function xhrHideNotifications(reply) {
    // Remove manually hidden users.
    const hiddenMaxAge = getHiddenMaxAge();
    const hiddenMinAge = getHiddenMinAge();
    const hiddenNames = getHiddenUsers();
    return reply.filter(x => filterUser(x.partner, hiddenMaxAge, hiddenMinAge, hiddenNames));
}

function xhrEnhanceUsers(reply) {
    // Remove manually hidden users.
    const enhancedTiles = getEnhancedTiles();
    const hiddenMaxAge = getHiddenMaxAge();
    const hiddenMinAge = getHiddenMinAge();
    const hiddenNames = getHiddenUsers();
    let newItems = [];
    for (let item of reply.items) {
        if (filterUser(item, hiddenMaxAge, hiddenMinAge, hiddenNames)) {
            // Show as "large tiles" to display user details everywhere.
            if (enhancedTiles)
                item.display.large_tile = true;
            newItems.push(item);
        }
    }
    reply.items = newItems;
    reply.items_total = reply.items.length;
    return reply
}

function xhrRestorePlusVisit(reply) {
    // Restore PLUS-visible visitors.
    reply.items_limited = 0;
    // Cosmetic patch #1
    if (!reply.is_plus) {
        reply.is_plus = true
        reply.is_free_plus = true // maybe not needed
        reply.payment_group = "PLUS"
    }
    // Cosmetic patch #2
    if (reply.inferface) {
        reply.show_plus_badge = true // maybe not needed
        reply.show_ads = false  // maybe not needed
    }
    // Cosmetic patch #3
    if (reply.show_plus_badge) {
        reply.show_plus_badge = true // maybe not needed
    }
    return reply
}

// ---- Tile UI ----

waitForKeyElements(
    "a.tile__link, " +
    "div.js-profiles a.listresult, " +
    "div.js-wrapper a.tile__link > div.tile__image, " +
    "#visits a.listresult", jNode => {
        // Determine tile properties.
        const tile = jNode.parent(".tile");
        const tileLink = tile.children(".tile__link").first();
        if (!tileLink) // ignore placeholders
            return;
        const tileImage = tileLink.children(".tile__image").first();
        const username = tileImage.attr("aria-label");
        // Add action bar.
        const tileBar = $("<div class='tile__bar'></div>").appendTo(tileLink);
        addShowImageAction(tileBar, tileImage);
        addHideUserAction(tileBar, tile, username);
    });

function addShowImageAction(tileBar, tileImage) {
    const style = tileImage.attr("style");
    if (!style)
        return;
    const url = style.substring(style.lastIndexOf("/") + 1, style.lastIndexOf(")"));
    if (url.endsWith(".svg")) // ignore "no photo" placeholders
        return;
    const origUrl = "/img/usr/original/0x0/" + url;
    $("<a class='tile__bar_action' href='" + origUrl + "' title='" + getString("viewFullImage") + "'><span class='icon icon-picture'></a>")
        .on("click", e => {
            e.preventDefault();
            window.open(origUrl, "_blank");
        })
        .appendTo(tileBar);
}

function addHideUserAction(tileBar, tile, username) {
    $("<a class='tile__bar_action' href='#' title='" + getString("hideUser") + "'><span class='icon icon-hide-visit'></a>")
        .on("click", e => {
            e.preventDefault();
            setUserHidden(username, true);
            tile.css("display", "none");
        })
        .appendTo(tileBar);
}

// ---- Settings UI ----

waitForKeyElements("li.js-settings > div.accordion > ul", jNode => {
    let itemClass = jNode.find("a").attr("class");
    $("<li><div><a class='" + itemClass + "'>" + getString("extensionTitle") + "</a></div></li>")
        .on("click", e => {
            // Force open the setting pane and clear any existing contents.
            $("#offcanvas-nav > .js-layer-content").addClass("is-open");
            const pane = $(".js-side-content");
            pane.empty();
            // Add pane and list.
            pane.append(`
<div class='layout layout--vertical layout--consume'>
    <div class='layout-item layout-item--consume layout layout--vertical'>

        <div class='layout-item settings__navigation p l-hidden-sm'>
            <div class='js-title typo-section-navigation'>` + getString("extensionTitle") + `</div>
        </div>

        <div class='layout-item layout-item--consume'>
            <div class='js-content js-scrollable fit scrollable'>
                <div class="p">
                    <div class="settings__key">
                        <div class="layout layout--v-center">
                            <div class="layout-item [ 6/12--sm ]">
                                <span>` + getString("enhancedTiles") + `</span>
                            </div>
                            <div class="layout-item [ 6/12--sm ]">
                                <div class="js-toggle-show-headlines pull-right">
                                    <div>
                                        <span class="ui-toggle ui-toggle--default ui-toggle--right">
                                            <input class="ui-toggle__input" type="checkbox" id="ra_enhancedTiles">
                                            <label class="ui-toggle__label" for="ra_enhancedTiles" style="touch-action: pan-y; user-select: none; -webkit-user-drag: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></label>
                                        </span>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div>
                            <div class="settings__description">` + getString("enhancedTilesDesc") + `</div>
                        </div>
                    </div>
                    <div class="settings__key">
                        <div>
                            <span>` + getString("hiddenUsers") + `</span>
                        </div>
                        <div class="separator separator--alt separator--narrow [ mb ] "></div>
                        <div class="settings__key">
                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>` + getString("minAge") + `</span>
                                </div>
                                <div class="layout-item [ 6/12--sm ]">
                                    <input class="input input--block" id="ra_hiddenMinAge" type="number" min="18" max="99"/>
                                </div>
                            </div>
                        </div>
                        <div class="settings__key">
                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>` + getString("maxAge") + `</span>
                                </div>
                                <div class="layout-item [ 6/12--sm ]">
                                    <input class="input input--block" id="ra_hiddenMaxAge" type="number" min="18" max="99"/>
                                </div>
                            </div>
                        </div>
                        <div class="settings__key">
                            <div class="js-grid-stats-selector">
                                <div>
                                    <ul class="js-list tags-list tags-list--centered" id="ra_hiddenUsers"/>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>`);
            // Handle enhanced user tiles.
            let inEnhancedTiles = $("#ra_enhancedTiles");
            inEnhancedTiles.prop("checked", getEnhancedTiles());
            inEnhancedTiles.on("change", e => {
                setEnhancedTiles(e.target.checked);
            });
            // Handle hidden age.
            let minAge = getHiddenMinAge();
            let maxAge = getHiddenMaxAge();
            let inMinAge = $("#ra_hiddenMinAge");
            let inMaxAge = $("#ra_hiddenMaxAge");
            inMinAge.val(minAge);
            inMaxAge.val(maxAge);
            inMinAge.on("change", e => {
                minAge = parseInt(e.target.value);
                setHiddenMinAge(minAge);
                if (minAge > maxAge) {
                    maxAge = minAge;
                    setHiddenMaxAge(maxAge);
                    inMaxAge.val(maxAge);
                }
            });
            inMaxAge.on("change", e => {
                maxAge = parseInt(e.target.value);
                setHiddenMaxAge(maxAge);
                if (maxAge < minAge) {
                    minAge = maxAge;
                    setHiddenMinAge(minAge);
                    inMinAge.val(minAge);
                }
            });
            // Handle hidden user list.
            const ul = $("#ra_hiddenUsers");
            for (const item of getHiddenUsers()) {
                const li = $("<li class='tags-list__item'/>").appendTo(ul);
                $("<a class='js-tag ui-tag ui-tag--removable ui-tag--selected' href='#'><span class='ui-tag__label'>" + item + "</span></a>")
                    .on("click", e => {
                        setUserHidden(e.target.innerHTML, false);
                        $(e.target).closest(".tags-list__item").css("display", "none");
                    })
                    .appendTo(li);
            };
        })
        .appendTo(jNode);
});