Romeo Additions

Adds some visitor-related features to PlanetRomeo.

As of 2021-05-13. See the latest version.

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 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        1.1
// @description    Adds some visitor-related features to PlanetRomeo.
// @description:de Fügt Besucher-bezogene Funktionen zu PlanetRomeo hinzu.
// @author         -Ray-
// @include        https://*.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(
	// Overrides
	"#visits > .layer__container--wider { width:unset; max-width:1227px; }" +
    "div[class*='tile--loading--'] .tile__image { background-image:url(/assets/05c2dc53b86dcd7abdb1d8a50346876b.svg); }" +
	// Custom styles
    ".tile:active { transform:unset; }" +
	".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 ====

const settingNs = "RA_SETTINGS:";

// ---- Language ----

const _strings = {
    "hiddenUsers": { "de": "Ausgeblendete Benutzer", "en": "Hidden users" },
    "hideUser": { "de": "Benutzer ausblenden", "en": "Hide user" },
    "viewFullSizeImage": { "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 + "%";
}

// ---- Actions for user tiles ----

var _hiddenUsers = JSON.parse(localStorage.getItem(settingNs + "hiddenUsers")) || [];
var _visibleAgeMin = 0;
var _visibleAgeMax = 200;

waitForKeyElements(
	"a.tile__link, " +
	"div.js-profiles a.listresult, " +
	"div.js-wrapper a.tile__link > div.tile__image, " +
	"#visits a.listresult", handleTile);

function handleTile(jNode) {
	const tile = jNode.parent(".tile");
	const tileLink = tile.children(".tile__link").first();
    if (!tileLink)
        return; // handle only real profile tiles, ignore placeholders
    const tileAge = tileLink.find(".typo-figure").first();
    const tileImage = tileLink.children(".tile__image").first();
    const age = parseInt(tileAge.text());
    const username = tileImage.attr("aria-label");
	// Remove if hidden.
	if (age < _visibleAgeMin || age > _visibleAgeMax || _hiddenUsers.includes(username)) {
        tile.remove();
		return;
	}
	// Unblur oldest visitors and restore their profile link and display name.
	if (tile.hasClass("tile--blurred")) {
		tile.removeClass("tile--blurred");
		tileLink.attr("href", "/profile/" + username);
		tileLink.append(
			"<div class='bg-raise tile__info'>" +
			"  <div class='info info--middle txt-raise'>" +
			"    <div class='txt-truncate layout-item--consume'>" +
			"      <div class='lh-heading txt-truncate'>" +
			"        <div class='info__main-data'>" +
			"          <div class='info__username' title='" + username + "' lang>" + username + "</div>" +
			"        </div>" +
			"      </div>" +
			"    </div>" +
			"  </div>" +
			"</div>" +
			"<div class='tile__footprint'>" +
			"  <div class='tile__visit '> &gt;1d </div>" +
			"</div>");
	}
	// Add actions.
	const tileBar = $("<div class='tile__bar'></div>").appendTo(tileLink);
	handleTile_addShowImage(tile, tileBar, tileImage);
	handleTile_addHideUser(tile, tileBar, username);
}

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

function handleTile_addHideUser(tile, tileBar, username) {
	$("<a class='tile__bar_action' href='#' title='" + getString("hideUser") + "'><span class='icon icon-remove-tag'></a>")
		.on("click", function(e) {
			if (!_hiddenUsers.includes(username)) {
				_hiddenUsers.push(username);
				_hiddenUsers.sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); });
				localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(_hiddenUsers));
			}
			e.preventDefault();
			$(e.target).closest(".tile").css("display", "none");
		})
		.appendTo(tileBar);
}

// ---- Settings ----

waitForKeyElements("li.js-settings > div.accordion > ul", handleSettings);

function handleSettings(jNode) {
	var itemClass = jNode.find("a").attr("class");
	$("<li><div><a class='" + itemClass + "'>" + getString("hiddenUsers") + "</a></div></li>")
		.on("click", function (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 item for each user.
			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("hiddenUsers") + "</div>" +
				"    </div>" +
				"    <div class='layout-item layout-item--consume'>" +
				"      <div class='js-content js-scrollable fit scrollable'>" +
				"        <div class='js-grid-stats-selector'>" +
				"          <div id='ra_hiddenusers'>" +
				"            <ul class='js-list tags-list tags-list--centered'/>" +
				"          </div>" +
				"        </div>" +
				"      </div>" +
				"    </div>" +
				"  </div>" +
				"</div>");
			const ul = $("#ra_hiddenusers > ul");
			_hiddenUsers.forEach(function (item, index) {
				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", function (e) {
						_hiddenUsers = _hiddenUsers.filter(function (ele) { return ele != e.target.innerHTML; });
						localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(_hiddenUsers));
						$(e.target).closest(".tags-list__item").css("display", "none");
					})
					.appendTo(li);
			});
		})
		.appendTo(jNode);
}