Romeo Additions

Adds some visitor-related features to PlanetRomeo.

Pada tanggal 15 Mei 2021. Lihat %(latest_version_link).

// ==UserScript==
// @name           Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        1.2
// @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__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:";

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

function unblurTile(tile, tileLink, username) {
	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>");
    }
}

// ---- Settings ----

var _visibleAgeMin = 0;
var _visibleAgeMax = 200;

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

function isUserHidden(username, age) {
    return age < _visibleAgeMin || age > _visibleAgeMax || getHiddenUsers().includes(username);
}

function toggleUser(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 = {
    "hiddenUsers": { "de": "Ausgeblendete Benutzer", "en": "Hidden users" },
    "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");
        // DEPRECATED: Restore oldest visitor's display names and links. Use Poor Men Plus instead to fully restore tiles.
        //unblurTile(tile, tileLink, username);
        // 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, isUserHidden(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-profile-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 = !isUserHidden(username);
            toggleUser(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("hiddenUsers") + "</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("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");
            // Add item for each user.
            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 => {
                        toggleUser(e.target.innerHTML, false);
                        $(e.target).closest(".tags-list__item").css("display", "none"); // breaks if fully removed?
                    })
                    .appendTo(li);
            };
        })
        .appendTo(jNode);
});