// ==UserScript==
// @name OLX Detailed Ratings
// @name:ro OLX Detalii Ratinguri
// @name:bg OLX Подробни Оценки
// @name:ua OLX Детальні Оцінки
// @name:pt OLX Avaliações Detalhadas
// @name:pl OLX Szczegółowe Oceny
// @description Shows detailed ratings for: olx.ro, olx.bg, olx.ua, olx.pt, and olx.pl
// @description:ro Detalii ratinguri pentru: olx.ro, olx.bg, olx.ua, olx.pt și olx.pl
// @description:bg Подробни оценки за: olx.ro, olx.bg, olx.ua, olx.pt и olx.pl
// @description:ua Детальні оцінки для: olx.ro, olx.bg, olx.ua, olx.pt та olx.pl
// @description:pt Mostra avaliações detalhadas para: olx.ro, olx.bg, olx.ua, olx.pt e olx.pl
// @description:pl Pokazuje szczegółowe oceny dla: olx.ro, olx.bg, olx.ua, olx.pt i olx.pl
// @author NWP
// @namespace https://greasyfork.org/users/877912
// @version 0.2
// @license MIT
// @match *://www.olx.ro/*
// @match *://www.olx.bg/*
// @match *://www.olx.ua/*
// @match *://www.olx.pt/*
// @match *://www.olx.pl/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
let debug = false;
let globalScore = null;
let maxScore = null;
let ratingsCount = null;
let user_score_data = null;
let translationRatings = null;
let lastCapturedTranslations = null;
let lastCapturedConfig = null;
let retriesCount = 0;
const maxRetries = 50;
// window.__PAGE_TRANSLATIONS is used for product page
// window.__INIT_CONFIG__ is used for user page
const isEmptyObject = (obj) => {
return obj && Object.keys(obj).length === 0 && obj.constructor === Object;
};
const log = (...args) => {
if (debug) {
console.log(...args);
}
};
const captureData = () => {
try {
if (window.__PAGE_TRANSLATIONS__ && window.__PAGE_TRANSLATIONS__ !== lastCapturedTranslations) {
lastCapturedTranslations = window.__PAGE_TRANSLATIONS__;
log('Captured window.__PAGE_TRANSLATIONS__:', lastCapturedTranslations);
handleWindowVariable();
}
if (window.__INIT_CONFIG__ && window.__INIT_CONFIG__ !== lastCapturedConfig) {
lastCapturedConfig = window.__INIT_CONFIG__;
log('Captured window.__INIT_CONFIG__:', lastCapturedConfig);
handleWindowVariable();
}
if (!translationRatings || Object.keys(translationRatings).length === 0) {
if (retriesCount < maxRetries) {
retriesCount++;
log(`Translation data not valid yet, retrying... (${retriesCount}/${maxRetries})`);
setTimeout(captureData, 100);
} else {
console.error('Max retries reached. Translation data could not be retrieved.');
}
}
} catch (error) {
console.error('Error in captureData:', error);
}
};
const originalFetch = window.fetch;
window.fetch = async (...args) => {
try {
const requestUrl = args[0];
log('Making API request with URL:', requestUrl);
if (requestUrl.startsWith("https://khonor.eu-sharedservices.olxcdn.com/api/olx/") &&
requestUrl.includes("/score/rating")) {
try {
const response = await originalFetch(...args);
log('API response received:', response);
const clonedResponse = response.clone();
clonedResponse.json().then((rating_data) => {
log('Rating data received:', rating_data);
user_score_data = rating_data.body[0];
globalScore = rating_data.body[0].data.score;
maxScore = rating_data.body[0].data.range.max;
ratingsCount = rating_data.body[0].data.ratings;
checkAndUpdateSentimentText();
}).catch((error) => {
console.error("Error reading response body:", error);
});
return response;
} catch (error) {
console.error('Error during fetch interception:', error);
return originalFetch(...args);
}
} else {
return originalFetch(...args);
}
} catch (error) {
console.error('Error in custom fetch handling:', error);
return originalFetch(...args);
}
};
function waitForElement(selector, callback, timeout = 10000) {
try {
const startTime = Date.now();
const checkInterval = setInterval(() => {
try {
let element;
if (window.location.href.startsWith("https://www.olx.ro/d/oferta")) {
const elements = document.querySelectorAll(selector);
element = elements.length > 1 ? elements[1] : null;
} else if (window.location.href.startsWith("https://www.olx.pl/d/oferta")) {
const elements = document.querySelectorAll(selector);
element = elements.length > 1 ? elements[1] : null;
} else {
element = document.querySelector(selector);
}
if (element) {
clearInterval(checkInterval);
callback(element);
} else if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
console.error(`Timeout: Element ${selector} not found within ${timeout}ms`);
}
} catch (error) {
console.error('Error in waitForElement interval check:', error);
clearInterval(checkInterval);
}
}, 100);
} catch (error) {
console.error('Error in waitForElement:', error);
}
}
function checkAndUpdateSentimentText() {
try {
if (!translationRatings || Object.keys(translationRatings).length === 0) {
if (retriesCount < maxRetries) {
retriesCount++;
log(`Translation data not ready yet. Deferring update... (${retriesCount}/${maxRetries})`);
setTimeout(checkAndUpdateSentimentText, 100);
} else {
console.error('Max retries reached. Could not update sentiment text.');
}
return;
}
updateSentimentText();
} catch (error) {
console.error('Error in checkAndUpdateSentimentText:', error);
}
}
function updateSentimentText() {
try {
const currentUrl = window.location.href;
let sentimentSpanSelector;
if (currentUrl.startsWith("https://www.olx.ro/d/oferta/")) {
sentimentSpanSelector = 'div.css-1k5snlb';
} else if (currentUrl.startsWith("https://www.olx.ro/oferte/")) {
sentimentSpanSelector = 'div.css-1k5snlb';
} else if (currentUrl.startsWith("https://www.olx.pl/d/oferta/")) {
sentimentSpanSelector = 'p.css-1usyphe';
} else if (currentUrl.startsWith("https://www.olx.pl/oferty/")) {
sentimentSpanSelector = 'p.css-1usyphe, p.css-1omjrm';
} else {
sentimentSpanSelector = 'span[data-testid="sentiment-title"]';
}
waitForElement(sentimentSpanSelector, (sentimentSpan) => {
try {
log('Found sentiment span:', sentimentSpan);
if (sentimentSpan && globalScore !== null && sentimentSpan.dataset.scoreUpdated !== "true") {
sentimentSpan.style.fontWeight = "bold";
sentimentSpan.style.color = "white";
sentimentSpan.dataset.scoreUpdated = "true";
const score = user_score_data.data.score;
const bucketSpec = user_score_data.bucketSpec;
const foundRange = bucketSpec.find((bucket) => score >= bucket.range.min && score <= bucket.range.max);
if (foundRange) {
const scoreContainer = document.createElement("div");
scoreContainer.style.display = "flex";
scoreContainer.style.flexDirection = "column";
scoreContainer.style.alignItems = "flex-start";
scoreContainer.style.marginTop = "0.3125rem";
if (currentUrl.startsWith("https://www.olx.pl/")) {
const oldRatingLabel = document.createElement("span");
oldRatingLabel.id = "old_rating";
oldRatingLabel.style.fontSize = "0.938rem";
oldRatingLabel.style.fontWeight = "bold";
oldRatingLabel.style.color = "white";
oldRatingLabel.textContent = `Stara ocena: ${translationRatings[user_score_data.data.label]}`;
scoreContainer.appendChild(oldRatingLabel);
}
const inlineContainer = document.createElement("div");
inlineContainer.style.display = "flex";
inlineContainer.style.alignItems = "center";
const fullRangeComparison = `[${foundRange.range.min} - ${score} - ${foundRange.range.max}]`;
const scoreText = document.createElement("span");
scoreText.id = "score";
scoreText.textContent = `[${globalScore}/${maxScore}] ${fullRangeComparison}`;
scoreText.style.fontSize = "0.875rem";
scoreText.style.fontWeight = "bold";
scoreText.style.color = "white";
inlineContainer.appendChild(scoreText);
appendSVG(inlineContainer);
scoreContainer.appendChild(inlineContainer);
const totalRatingsElement = document.querySelector('span[data-testid="total-ratings"]');
if (totalRatingsElement !== null) {
totalRatingsElement.style.fontSize = "0.875rem";
totalRatingsElement.style.fontWeight = "bold";
totalRatingsElement.style.color = "white";
} else {
const totalRatingsSpan = document.createElement("span");
totalRatingsSpan.id = "total_rating";
totalRatingsSpan.style.fontSize = "0.875rem";
totalRatingsSpan.style.fontWeight = "bold";
totalRatingsSpan.style.color = "white";
let language = document.querySelector('meta[http-equiv="content-language"]')?.getAttribute("content");
if (language == "pl") {
totalRatingsSpan.textContent = `(${ratingsCount} ${ratingsCount == 1 ? "stara ocena" : "stare oceny"})`;
} else if (language == "ro"){
totalRatingsSpan.textContent = `(${ratingsCount} ${ratingsCount == 1 ? "rating vechi" : "ratinguri vechi"})`;
} else {
totalRatingsSpan.textContent = `(${ratingsCount} ${ratingsCount == 1 ? "rating" : "ratings"})`;
}
scoreContainer.appendChild(totalRatingsSpan);
}
sentimentSpan.after(scoreContainer);
}
} else {
log('Sentiment span not found, globalScore is null, or score already updated');
}
} catch (error) {
console.error('Error in waitForElement callback (updateSentimentText):', error);
}
});
} catch (error) {
console.error('Error in updateSentimentText:', error);
}
}
function appendSVG(inlineContainer) {
try {
const svgNamespace = "http://www.w3.org/2000/svg";
const svgElement = document.createElementNS(svgNamespace, "svg");
svgElement.setAttribute("width", "1.2rem");
svgElement.setAttribute("height", "1.2rem");
svgElement.setAttribute("viewBox", "0 0 24 24");
svgElement.setAttribute("fill", "currentColor");
svgElement.id = "rating_legend_tooltip";
svgElement.style.marginTop = "0.05rem";
svgElement.style.marginLeft = "0.3125rem";
svgElement.style.cursor = "pointer";
svgElement.style.transform = 'rotate(180deg)';
const path = document.createElementNS(svgNamespace, "path");
path.setAttribute("d", "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z");
svgElement.appendChild(path);
inlineContainer.appendChild(svgElement);
const tooltip = document.createElement("div");
tooltip.className = "tooltip";
tooltip.style.position = "absolute";
tooltip.style.display = "none";
tooltip.style.border = "1px solid #ccc";
tooltip.style.backgroundColor = "#f9f9f9";
tooltip.style.padding = "0.625rem";
tooltip.style.zIndex = "1000";
document.body.appendChild(tooltip);
let tooltipVisible = false;
svgElement.addEventListener("click", (event) => {
try {
event.stopPropagation();
tooltipVisible = !tooltipVisible;
if (tooltipVisible) {
showTooltip(event, tooltip, svgElement);
} else {
hideTooltip(tooltip);
}
} catch (error) {
console.error('Error in SVG click event (appendSVG):', error);
}
});
document.addEventListener("click", (event) => {
try {
if (!svgElement.contains(event.target) && tooltipVisible) {
hideTooltip(tooltip);
tooltipVisible = false;
}
} catch (error) {
console.error('Error in document click event (appendSVG):', error);
}
}, true);
svgElement.style.fill = 'white';
svgElement.addEventListener('click', function(event) {
event.preventDefault();
});
svgElement.addEventListener('click', function(event) {
event.stopPropagation();
});
} catch (error) {
console.error('Error in appendSVG:', error);
}
}
function showTooltip(event, tooltip, svgElement) {
try {
event.stopPropagation();
updateTooltipContent(tooltip);
const svgRect = svgElement.getBoundingClientRect();
tooltip.style.left = `${svgRect.right + window.scrollX}px`;
tooltip.style.top = `${svgRect.bottom + window.scrollY}px`;
tooltip.style.display = "block";
} catch (error) {
console.error('Error in showTooltip:', error);
}
}
function updateTooltipContent(tooltip) {
try {
let tooltipContent = "";
if (!translationRatings || Object.keys(translationRatings).length === 0) {
if (retriesCount < maxRetries) {
retriesCount++;
log(`Translation data not available, retrying... (${retriesCount}/${maxRetries})`);
setTimeout(() => {
updateTooltipContent(tooltip);
}, 100);
} else {
console.error('Max retries reached. Could not update tooltip content.');
}
return;
}
user_score_data.bucketSpec.slice().reverse().forEach((bucket) => {
if (bucket.bucketName !== "none" && bucket.range.min !== null && bucket.range.max !== null) {
tooltipContent += `<p style="margin-top: 0.3125rem; margin-bottom: 0.3125rem;"><strong>${translationRatings[bucket.bucketName] || bucket.bucketName}</strong>: ${bucket.range.min} - ${bucket.range.max}</p>`;
}
});
tooltip.innerHTML = tooltipContent;
} catch (error) {
console.error('Error in updateTooltipContent:', error);
}
}
function hideTooltip(tooltip) {
try {
tooltip.style.display = "none";
} catch (error) {
console.error('Error in hideTooltip:', error);
}
}
const handleWindowVariable = () => {
try {
let translationsValid = false;
if (window.__PAGE_TRANSLATIONS__ && !isEmptyObject(window.__PAGE_TRANSLATIONS__)) {
const ratingDataNames = JSON.parse(window.__PAGE_TRANSLATIONS__);
if (ratingDataNames.pageTranslations &&
ratingDataNames.pageTranslations.adview &&
ratingDataNames.pageTranslations.adview["srt.rating.superTitle"] &&
ratingDataNames.pageTranslations.adview["srt.rating.goodTitle"] &&
ratingDataNames.pageTranslations.adview["srt.rating.fairTitle"] &&
ratingDataNames.pageTranslations.adview["srt.rating.poorTitle"]) {
translationRatings = {
super: ratingDataNames.pageTranslations.adview["srt.rating.superTitle"],
good: ratingDataNames.pageTranslations.adview["srt.rating.goodTitle"],
fair: ratingDataNames.pageTranslations.adview["srt.rating.fairTitle"],
poor: ratingDataNames.pageTranslations.adview["srt.rating.poorTitle"],
};
log('%cwindow.__PAGE_TRANSLATIONS__ found and valid:', 'color: green;', translationRatings);
translationsValid = true;
} else {
console.warn('window.__PAGE_TRANSLATIONS__ found but missing required translation keys.');
}
}
if (!translationsValid && window.__INIT_CONFIG__) {
const ratingDataNames = JSON.parse(window.__INIT_CONFIG__);
const locale = ratingDataNames.appConfig.locale;
translationRatings = {
super: ratingDataNames.language.messages[locale]["srt.rating.superTitle"],
good: ratingDataNames.language.messages[locale]["srt.rating.goodTitle"],
fair: ratingDataNames.language.messages[locale]["srt.rating.fairTitle"],
poor: ratingDataNames.language.messages[locale]["srt.rating.poorTitle"],
};
log('%cwindow.__INIT_CONFIG__ used as fallback:', 'color: red;', translationRatings);
}
checkAndUpdateSentimentText();
} catch (error) {
console.error('Error in handleWindowVariable:', error);
}
};
const observer = new MutationObserver((mutationsList, observer) => {
try {
for (let mutation of mutationsList) {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
try {
captureData();
} catch (error) {
console.error('Error in captureData during MutationObserver:', error);
}
}
}
if (translationRatings && Object.keys(translationRatings).length > 0) {
log('Translation data captured. Disconnecting observer.');
observer.disconnect();
}
} catch (error) {
console.error('Error in MutationObserver callback:', error);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true
});
document.addEventListener('DOMContentLoaded', () => {
try {
captureData();
} catch (error) {
console.error('Error on DOMContentLoaded:', error);
}
});
log('OLX Rating Modifier Script Initialized');
})();