· 4 years ago · Aug 23, 2021, 01:36 AM
1// ==UserScript==
2// @name Romeo Additions
3// @namespace https://greasyfork.org/en/users/723211-ray/
4// @version 2.2
5// @description Allows to hide users, display their information on tiles, and enhances the Radar.
6// @description:de Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.
7// @author -Ray-, Djamana
8// @include *://*.romeo.com/*
9// @grant GM_addStyle
10// @require https://code.jquery.com/git/jquery-git.slim.min.js
11// @_require https://code.jquery.com/jquery-3.6.0.slim.min.js
12// ==/UserScript==
13
14// ==== Dependencies ====
15
16/*! waitForKeyElements | https://gist.github.com/BrockA/2625891 */
17/*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
18 that detects and handles AJAXed content.
19 Usage example:
20 waitForKeyElements (
21 "div.comments"
22 , commentCallbackFunction
23 );
24 //--- Page-specific function to do what we want when the node is found.
25 function commentCallbackFunction (jNode) {
26 jNode.text ("This comment changed by waitForKeyElements().");
27 }
28 IMPORTANT: This function requires your script to have loaded jQuery.
29*/
30function waitForKeyElements(
31 selectorTxt, /* Required: The jQuery selector string that
32 specifies the desired element(s).
33 */
34 actionFunction, /* Required: The code to run when elements are
35 found. It is passed a jNode to the matched
36 element.
37 */
38 bWaitOnce, /* Optional: If false, will continue to scan for
39 new elements even after the first match is
40 found.
41 */
42 iframeSelector /* Optional: If set, identifies the iframe to
43 search.
44 */
45) {
46 var targetNodes, btargetsFound;
47
48 if (typeof iframeSelector == "undefined")
49 targetNodes = $(selectorTxt);
50 else
51 targetNodes = $(iframeSelector).contents()
52 .find(selectorTxt);
53
54 if (targetNodes && targetNodes.length > 0) {
55 btargetsFound = true;
56 /*--- Found target node(s). Go through each and act if they
57 are new.
58 */
59 targetNodes.each(function () {
60 var jThis = $(this);
61 var alreadyFound = jThis.data('alreadyFound') || false;
62
63 if (!alreadyFound) {
64 //--- Call the payload function.
65 var cancelFound = actionFunction(jThis);
66 if (cancelFound)
67 btargetsFound = false;
68 else
69 jThis.data('alreadyFound', true);
70 }
71 });
72 }
73 else {
74 btargetsFound = false;
75 }
76
77 //--- Get the timer-control variable for this selector.
78 var controlObj = waitForKeyElements.controlObj || {};
79 var controlKey = selectorTxt.replace(/[^\w]/g, "_");
80 var timeControl = controlObj[controlKey];
81
82 //--- Now set or clear the timer as appropriate.
83 if (btargetsFound && bWaitOnce && timeControl) {
84 //--- The only condition where we need to clear the timer.
85 clearInterval(timeControl);
86 delete controlObj[controlKey]
87 }
88 else {
89 //--- Set a timer, if needed.
90 if (!timeControl) {
91 timeControl = setInterval(function () {
92 waitForKeyElements(selectorTxt,
93 actionFunction,
94 bWaitOnce,
95 iframeSelector
96 );
97 },
98 300
99 );
100 controlObj[controlKey] = timeControl;
101 }
102 }
103 waitForKeyElements.controlObj = controlObj;
104}
105
106// ==== CSS ====
107
108GM_addStyle(`
109#visits > .layer__container--wider { width:unset; max-width:1227px; }
110div[class*='tile--loading--'] .tile__image { background-image:url(/assets/05c2dc53b86dcd7abdb1d8a50346876b.svg); }
111
112.tile__bar { position:absolute; bottom:0; right:0; visibility:hidden; }
113.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.45rem; }
114.tile__bar_action:hover { background-color:#00A3E4; }
115.tile__bar_action:active { background-color:#06648B; }
116.tile__link:hover .tile__bar { visibility:visible; }
117`);
118
119// ==== Script ====
120
121(function () {
122 'use strict';
123 proxyXhr();
124})();
125
126// ---- Language ----
127
128const _strings = {
129 "display": {
130 "de": "Anzeige",
131 "en": "Display"
132 },
133 "enhancedTiles": {
134 "de": "Erweiterte Kacheln",
135 "en": "Enhanced tiles"
136 },
137 "enhancedTilesDesc": {
138 "de": "Zeige alle Benutzerdetails auf den Kacheln. Im Radar wird dies Benutzer als 'Plus'-Abonnenten mit großen Kacheln darstellen.",
139 "en": "Shows all user details on tiles. The radar will claim users to be 'Plus' subscribers in large tiles."
140 },
141 "extensionTitle": {
142 "en": "Romeo Additions"
143 },
144 "hiddenUsers": {
145 "de": "Ausgeblendete Benutzer",
146 "en": "Hidden users"
147 },
148 "hideUser": {
149 "de": "Benutzer ausblenden",
150 "en": "Hide user"
151 },
152 "maxAge": {
153 "de": "Maximales Alter",
154 "en": "Maximal age"
155 },
156 "minAge": {
157 "de": "Minimales Alter",
158 "en": "Minimal age"
159 },
160 "viewFullImage": {
161 "de": "Bild vergrößern",
162 "en": "View full image"
163 },
164}
165
166function getString(key) {
167 const lang = document.documentElement.getAttribute("lang") || "en";
168 const translations = _strings[key];
169 if (translations)
170 return translations[lang] || translations["en"] || "%" + key + "%";
171 return "%" + key + "%";
172}
173
174// ---- Settings ----
175
176const settingNs = "RA_SETTINGS:";
177
178function getEnhancedTiles() {
179 return localStorage.getItem(settingNs + "enhancedTiles") == "true";
180}
181
182function getHiddenMaxAge() {
183 return parseInt(localStorage.getItem(settingNs + "hiddenMaxAge")) || 99;
184}
185
186function getHiddenMinAge() {
187 return parseInt(localStorage.getItem(settingNs + "hiddenMinAge")) || 18;
188}
189
190function getHiddenUsers() {
191 return JSON.parse(localStorage.getItem(settingNs + "hiddenUsers")) || [];
192}
193
194function setEnhancedTiles(value) {
195 localStorage.setItem(settingNs + "enhancedTiles", value);
196}
197
198function setHiddenMaxAge(value) {
199 localStorage.setItem(settingNs + "hiddenMaxAge", value);
200}
201
202function setHiddenMinAge(value) {
203 localStorage.setItem(settingNs + "hiddenMinAge", value);
204}
205
206function setUserHidden(username, hide) {
207 let hiddenUsers = getHiddenUsers();
208 if (hide) {
209 if (hiddenUsers.length < hiddenUsers.push(username)) {
210 hiddenUsers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
211 localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(hiddenUsers));
212 }
213 } else {
214 const prevLength = hiddenUsers.length;
215 hiddenUsers = hiddenUsers.filter(e => e != username);
216 if (prevLength > hiddenUsers.length)
217 localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(hiddenUsers));
218 }
219}
220
221// ---- XHR ----
222
223function ReMaskDash(RE_WithDashes) {
224 return RE_WithDashes
225 .replaceAll("/","\\/")
226}
227
228function isApiRequest(url, verb) {
229 // Request must start with "/api/v?/" or "/api/+/" followed by the given verb.
230 //const matches = url.match(/\/api\/(v[0-9]|\+)\//);
231 const matches = url.match(ReMaskDash("/api/(v[0-9]|\\+)/"));
232 if (matches && matches.length)
233 return url.startsWith(verb, matches[0].length);
234 return false;
235}
236
237function filterUser(user, hiddenMaxAge, hiddenMinAge, hiddenNames) {
238 return user.personal.age >= hiddenMinAge
239 && user.personal.age <= hiddenMaxAge
240 && !hiddenNames.includes(user.name)
241}
242
243function proxyXhr() {
244 // Intercept XHR queries and replies by hooking the XHR open method.
245 const realOpen = window.XMLHttpRequest.prototype.open;
246 window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
247 this.addEventListener("load", () => {
248 //console.log("[RA] XHR reply: method=" + method + ", url=" + url);
249 try {
250 // Parse data.
251 const isString = typeof this.response === "string";
252 reply = isString ?
253 JSON.parse(this.response) :
254 this.response;
255
256 // Modify interesting data.
257 if ( isApiRequest(url, "notifications") ) {
258 reply = xhrHideNotifications(reply);
259 }
260 if (
261 isApiRequest(url, "profiles") ||
262 isApiRequest(url, "visitors") ||
263 isApiRequest(url, "visits")
264 ) {
265 reply = xhrRestorePlusVisit( reply );
266 reply = xhrEnhanceUsers ( reply );
267
268 }
269 if ( isApiRequest(url, "session") ) {
270 reply = xhrPoorMensPlus( reply );
271 }
272
273
274 // Write back possibly modified data.
275 Object.defineProperty(this, "responseText", { writable: true });
276 this.responseText = isString ? JSON.stringify(reply) : reply;
277 } catch (e) {
278 //console.log("[RA] XHR handler failed: " + e)
279 }
280 });
281
282 // Forward to client.
283 return realOpen.apply(this, arguments);
284 }
285}
286
287function xhrHideNotifications(reply) {
288 // Remove manually hidden users.
289 const hiddenMaxAge = getHiddenMaxAge();
290 const hiddenMinAge = getHiddenMinAge();
291 const hiddenNames = getHiddenUsers();
292 return reply.filter(x => filterUser(x.partner, hiddenMaxAge, hiddenMinAge, hiddenNames));
293}
294
295function xhrEnhanceUsers(reply) {
296 // Remove manually hidden users.
297 const enhancedTiles = getEnhancedTiles();
298 const hiddenMaxAge = getHiddenMaxAge();
299 const hiddenMinAge = getHiddenMinAge();
300 const hiddenNames = getHiddenUsers();
301 let newItems = [];
302 for (let item of reply.items) {
303 if (filterUser(item, hiddenMaxAge, hiddenMinAge, hiddenNames)) {
304 // Show as "large tiles" to display user details everywhere.
305 if (enhancedTiles)
306 item.display.large_tile = true;
307 newItems.push(item);
308 }
309 }
310 reply.items = newItems;
311 return reply;
312}
313
314
315function xhrPoorMensPlus(reply) {
316 // Cosmetic patch #1
317 if (!reply.is_plus) {
318 reply.is_plus = true
319 reply.is_free_plus = true // maybe not needed
320 reply.payment_group = "PLUS"
321 }
322 // Cosmetic patch #2
323 if (reply.inferface) {
324 reply.show_plus_badge = true // maybe not needed
325 reply.show_ads = false // maybe not needed
326 }
327 // Cosmetic patch #3
328 if (reply.show_plus_badge) {
329 reply.show_plus_badge = true // maybe not needed
330 }
331 return reply;
332}
333function xhrRestorePlusVisit(reply) {
334 // Restore PLUS-visible visitors.
335 reply.items_limited = 0;
336 return reply;
337}
338
339// ---- Tile UI ----
340
341waitForKeyElements(
342 "a.tile__link, " +
343 "div.js-profiles a.listresult, " +
344 "div.js-wrapper a.tile__link > div.tile__image, " +
345 "#visits a.listresult", jNode => {
346 // Determine tile properties.
347 const tile = jNode.parent(".tile");
348 const tileLink = tile.children(".tile__link").first();
349 if (!tileLink) // ignore placeholders
350 return;
351 // Add full headline as tooltip.
352 const tileInfo = tileLink.children(".tile__info").first();
353 const tileHeadline = tileInfo.children(".tile__headline").first();
354 tileHeadline.attr("title", tileHeadline.text());
355 // Add action bar.
356 const tileImage = tileLink.children(".tile__image").first();
357 const username = tileImage.attr("aria-label");
358 const tileBar = $("<div class='tile__bar'></div>").appendTo(tileLink);
359 addShowImageAction(tileBar, tileImage);
360 addHideUserAction(tileBar, tile, username);
361 });
362
363function addShowImageAction(tileBar, tileImage) {
364 const style = tileImage.attr("style");
365 if (!style)
366 return;
367 const url = style.substring(style.lastIndexOf("/") + 1, style.lastIndexOf(")"));
368 if (url.endsWith(".svg")) // ignore "no photo" placeholders
369 return;
370 const origUrl = "/img/usr/original/0x0/" + url;
371 $("<a class='tile__bar_action' href='" + origUrl + "' title='" + getString("viewFullImage") + "'><span class='icon icon-picture'></a>")
372 .on("click", e => {
373 e.preventDefault();
374 window.open(origUrl, "_blank");
375 })
376 .appendTo(tileBar);
377}
378
379function addHideUserAction(tileBar, tile, username) {
380 $("<a class='tile__bar_action' href='#' title='" + getString("hideUser") + "'><span class='icon icon-hide-visit'></a>")
381 .on("click", e => {
382 e.preventDefault();
383 setUserHidden(username, true);
384 tile.css("display", "none");
385 })
386 .appendTo(tileBar);
387}
388
389// ---- Settings UI ----
390
391waitForKeyElements("li.js-settings > div.accordion > ul", jNode => {
392 let itemClass = jNode.find("a").attr("class");
393 $("<li><div><a class='" + itemClass + "'>" + getString("extensionTitle") + "</a></div></li>")
394 .on("click", e => {
395 // Force open the setting pane and clear any existing contents.
396 $("#offcanvas-nav > .js-layer-content").addClass("is-open");
397 const pane = $(".js-side-content");
398 pane.empty();
399 // Add pane and list.
400 pane.append(`
401<div class='layout layout--vertical layout--consume'>
402 <div class='layout-item layout-item--consume layout layout--vertical'>
403
404 <div class='layout-item settings__navigation p l-hidden-sm'>
405 <div class='js-title typo-section-navigation'>` + getString("extensionTitle") + `</div>
406 </div>
407
408 <div class='layout-item layout-item--consume'>
409 <div class='js-content js-scrollable fit scrollable'>
410 <div class="p">
411 <div class="settings__key">
412 <div class="layout layout--v-center">
413 <div class="layout-item [ 6/12--sm ]">
414 <span>` + getString("enhancedTiles") + `</span>
415 </div>
416 <div class="layout-item [ 6/12--sm ]">
417 <div class="js-toggle-show-headlines pull-right">
418 <div>
419 <span class="ui-toggle ui-toggle--default ui-toggle--right">
420 <input class="ui-toggle__input" type="checkbox" id="ra_enhancedTiles">
421 <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>
422 </span>
423 </div>
424 </div>
425 </div>
426 </div>
427 <div>
428 <div class="settings__description">` + getString("enhancedTilesDesc") + `</div>
429 </div>
430 </div>
431 <div class="settings__key">
432 <div>
433 <span>` + getString("hiddenUsers") + `</span>
434 </div>
435 <div class="separator separator--alt separator--narrow [ mb ] "></div>
436 <div class="settings__key">
437 <div class="layout layout--v-center">
438 <div class="layout-item [ 6/12--sm ]">
439 <span>` + getString("minAge") + `</span>
440 </div>
441 <div class="layout-item [ 6/12--sm ]">
442 <input class="input input--block" id="ra_hiddenMinAge" type="number" min="18" max="99"/>
443 </div>
444 </div>
445 </div>
446 <div class="settings__key">
447 <div class="layout layout--v-center">
448 <div class="layout-item [ 6/12--sm ]">
449 <span>` + getString("maxAge") + `</span>
450 </div>
451 <div class="layout-item [ 6/12--sm ]">
452 <input class="input input--block" id="ra_hiddenMaxAge" type="number" min="18" max="99"/>
453 </div>
454 </div>
455 </div>
456 <div class="settings__key">
457 <div class="js-grid-stats-selector">
458 <div>
459 <ul class="js-list tags-list tags-list--centered" id="ra_hiddenUsers"/>
460 </div>
461 </div>
462 </div>
463 </div>
464 </div>
465 </div>
466 </div>
467 </div>
468</div>`);
469 // Handle enhanced user tiles.
470 let inEnhancedTiles = $("#ra_enhancedTiles");
471 inEnhancedTiles.prop("checked", getEnhancedTiles());
472 inEnhancedTiles.on("change", e => {
473 setEnhancedTiles(e.target.checked);
474 });
475 // Handle hidden age.
476 let minAge = getHiddenMinAge();
477 let maxAge = getHiddenMaxAge();
478 let inMinAge = $("#ra_hiddenMinAge");
479 let inMaxAge = $("#ra_hiddenMaxAge");
480 inMinAge.val(minAge);
481 inMaxAge.val(maxAge);
482 inMinAge.on("change", e => {
483 minAge = parseInt(e.target.value);
484 setHiddenMinAge(minAge);
485 if (minAge > maxAge) {
486 maxAge = minAge;
487 setHiddenMaxAge(maxAge);
488 inMaxAge.val(maxAge);
489 }
490 });
491 inMaxAge.on("change", e => {
492 maxAge = parseInt(e.target.value);
493 setHiddenMaxAge(maxAge);
494 if (maxAge < minAge) {
495 minAge = maxAge;
496 setHiddenMinAge(minAge);
497 inMinAge.val(minAge);
498 }
499 });
500 // Handle hidden user list.
501 const ul = $("#ra_hiddenUsers");
502 for (const item of getHiddenUsers()) {
503 const li = $("<li class='tags-list__item'/>").appendTo(ul);
504 $("<a class='js-tag ui-tag ui-tag--removable ui-tag--selected' href='#'><span class='ui-tag__label'>" + item + "</span></a>")
505 .on("click", e => {
506 setUserHidden(e.target.innerHTML, false);
507 $(e.target).closest(".tags-list__item").css("display", "none");
508 })
509 .appendTo(li);
510 };
511 })
512 .appendTo(jNode);
513});
514