Romeo Additions

Adds features to PlanetRomeo user and visitor tiles.

Stan na 16-05-2021. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name           Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        1.3
// @description    Adds features to PlanetRomeo user and visitor tiles.
// @description:de Neue Funktionen für PlanetRomeo Benutzer- und Besucherkacheln.
// @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); 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();
})();

function toggleTile(tile, tileImage, hide) {
    tile.css("filter", hide ? "blur(1px) grayscale(100%) opacity(25%)" : "");
    tileImage.css("filter", hide ? "blur(16px) contrast(66%)" : "");
}

function proxyXHR() {
    // Added from "Poor Men's Plus" as requested by Djamana
    let oldXHROpen = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
        // do something with the method, url and etc.
        this.addEventListener("load", () => {
            // do something with the response text
            isResponseAString = (typeof this.response === "string")
            try {
                this.xhr = this.response
                if (isResponseAString)
                    this.xhr = JSON.parse(this.xhr)

                // remove displaylimit in visitors tab       <- that's the core patch !
                if (this.xhr.items_limited)
                    delete this.xhr.items_limited;

                try {
                    // cosmetic patch #1 (not really needed)
                    if (this.xhr.is_plus == false) {
                        this.xhr.is_plus = true
                        this.is_free_plus = true // maybe not needed
                        this.xhr.payment_group = "PLUS"
                    }
                    // cosmetic patch #2 (not really needed)
                    if (this.xhr.inferface) {
                        this.xhr.show_plus_badge = true // maybe not needed
                        this.xhr.show_ads = false  // maybe not needed
                    }
                    // cosmetic patch #3 (not really needed)
                    if (this.xhr.show_plus_badge) {
                        this.xhr.show_plus_badge = true // maybe not needed
                    }
                } catch (e) {
                    console.log("RomeoAdditions > restoreVisitorTiles (cosmetic patches) - ERROR: " + e)
                }

                // make responseText writeable
                Object.defineProperty(this, 'responseText', { writable: true });
                // set responseText
                if (isResponseAString)
                    this.responseText = JSON.stringify(this.xhr)
                else
                    this.responseText = this.xhr

            } catch (e) {
                if (!(e instanceof SyntaxError))
                    console.log("RomeoAdditions > restoreVisitorTiles - ERROR: " + e)
            }
        });
        return oldXHROpen.apply(this, arguments);
    }
}

// ---- Settings ----

const settingNs = "RA_SETTINGS:";

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 getUserHidden(username, age) {
    return age < getHiddenMinAge() || age > getHiddenMaxAge() || getHiddenUsers().includes(username);
}

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));
    }
}

// ---- Language ----

const _strings = {
    "display": {
        "de": "Anzeige",
        "en": "Display"
    },
    "extensionTitle": {
        "en": "Romeo Additions"
    },
    "hideUsers": {
        "de": "Benutzer ausblenden",
        "en": "Hide users"
    },
    "maxAge": {
        "de": "Maximales Alter",
        "en": "Maximal age"
    },
    "minAge": {
        "de": "Minimales Alter",
        "en": "Minimal age"
    },
    "toggleUser": {
        "de": "Benutzer ausblenden / anzeigen",
        "en": "Hide / show user"
    },
    "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 + "%";
}

// ---- Tile handler ----

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 age = parseInt(tileLink.find(".typo-figure").first().text());
        const username = tileImage.attr("aria-label");
        // Add action bar.
        const tileBar = $("<div class='tile__bar'></div>").appendTo(tileLink);
        addShowImageAction(tileBar, tileImage);
        addToggleUserAction(tileBar, tile, tileImage, username);
        // Set initial state.
        toggleTile(tile, tileImage, getUserHidden(username, age));
    });

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 addToggleUserAction(tileBar, tile, tileImage, username) {
    $("<a class='tile__bar_action' href='#' title='" + getString("toggleUser") + "'><span class='icon icon-hide-visit'></a>")
        .on("click", e => {
            e.preventDefault();
            hide = !getUserHidden(username);
            setUserHidden(username, hide);
            toggleTile(tile, tileImage, hide);
        })
        .appendTo(tileBar);
}

// ---- Settings handler ----

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>
                            <span>` + getString("hideUsers") + `</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 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 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"); // breaks if fully removed?
                    })
                    .appendTo(li);
            };
        })
        .appendTo(jNode);
});