Zamienia tytuły na stronie Onet.pl na mniej click-baitowe
// ==UserScript==
// @name Onet De-Baiter
// @version 1.3.4
// @description Zamienia tytuły na stronie Onet.pl na mniej click-baitowe
// @match *://www.onet.pl/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @connect debaiter.westeurope.cloudapp.azure.com
// @namespace https://greasyfork.org/users/1575957
// ==/UserScript==
(function() {
'use strict';
// --- KONFIGURACJA ---
// Selektor elementów, w których skrypt szuka linków i nagłówków H3
const ARTICLE_SELECTOR = 'article a[href^="https://www.onet.pl/"], article a[href^="https://wiadomosci.onet.pl/"]';
// --- SEKCJA CSS ---
// Przeniesione style dla przetworzonych nagłówków
GM_addStyle(`
.debaiter-processed {
text-decoration: underline dotted currentColor !important;
text-underline-offset: 4px !important;
}
`);
// --- SEKCJA CACHE ---
// Zwraca zbuforowany tytuł dla danego hasha lub null, jeśli nie istnieje
async function getFromCache(hash) {
return GM_getValue(hash, null);
}
// Zapisuje nowy tytuł do pamięci podręcznej
async function saveToCache(hash, title) {
GM_setValue(hash, title);
}
// --- FUNKCJE POMOCNICZE I MODYFIKUJĄCE ---
// Funkcja wyliczająca SHA-256
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// Funkcja aktualizująca H3 i dodająca klasę "przetworzony"
function updateH3(h3Element, newTitle) {
h3Element.title = h3Element.innerText;
h3Element.innerText = newTitle;
// Przypisanie klasy CSS zamiast stylów inline
h3Element.classList.add('debaiter-processed');
}
// --- LOGIKA GŁÓWNA ---
// Zmienne do grupowania (batching) zapytań
let pendingPayload = [];
let pendingElementsMap = new Map();
let batchTimeout = null;
// Funkcja wysyłająca zgromadzoną paczkę danych na serwer
function sendBatch() {
if (pendingPayload.length === 0) return;
// Kopiujemy obecny stan do wysyłki i od razu czyścimy globalne zmienne na poczet kolejnych artykułów
const payloadToSend = [...pendingPayload];
const mapForThisBatch = new Map(pendingElementsMap);
pendingPayload = [];
pendingElementsMap.clear();
GM_xmlhttpRequest({
method: "POST",
url: "https://debaiter.westeurope.cloudapp.azure.com/api/v1/",
data: JSON.stringify(payloadToSend),
headers: {
"Content-Type": "application/json"
},
onload: function(response) {
try {
const newTitles = JSON.parse(response.responseText);
for (const [hash, newTitle] of Object.entries(newTitles)) {
if (mapForThisBatch.has(hash)) {
updateH3(mapForThisBatch.get(hash), newTitle);
saveToCache(hash, newTitle); // Zapis do cache po otrzymaniu z serwera
}
}
} catch (error) {
console.error("De-Baiter: Błąd parsowania odpowiedzi", error);
console.error(response.responseText);
}
},
onerror: function(error) {
console.error("De-Baiter: Błąd sieci podczas wysyłania", error);
}
});
}
// Funkcja szukająca i przetwarzająca nowe elementy na stronie
async function processElements() {
// Używamy zmiennej ARTICLE_SELECTOR zdefiniowanej na górze skryptu
const articles = document.querySelectorAll(ARTICLE_SELECTOR);
let addedNew = false;
for (const a of articles) {
// Zapobiegamy ponownemu przetwarzaniu tego samego artykułu (np. przy scrollowaniu)
if (a.dataset.debaiterProcessed) continue;
const h3 = a.querySelector('h3');
if (h3) {
const title = h3.innerText.trim();
if (title && a.href) {
// Oznaczamy element jako już znaleziony
a.dataset.debaiterProcessed = "true";
// Oczyszczamy URL z parametrów śledzących (np. ?srcc=...) dla spójnego hasha
const cleanHref = a.href.split('?')[0];
const hash = await sha256(cleanHref); // Hash z czystego hrefa
// Wstępna obsługa cache - sprawdzamy przed dodaniem do kolejki
const cachedTitle = await getFromCache(hash);
if (cachedTitle) {
updateH3(h3, cachedTitle);
} else {
pendingElementsMap.set(hash, h3);
pendingPayload.push({
hash: hash,
title: title,
href: cleanHref // Wysyłamy czysty href
});
addedNew = true;
}
}
}
}
// Jeśli znaleziono nowe tytuły (niepochodzące z cache), restartujemy odliczanie (debounce).
// Zapytanie poleci 500ms po pojawieniu się ostatniego elementu w oknie.
if (addedNew) {
if (batchTimeout) clearTimeout(batchTimeout);
batchTimeout = setTimeout(sendBatch, 500);
}
}
// 1. Uruchomienie dla elementów już istniejących w momencie ładowania
processElements();
// 2. Użycie MutationObserver do reagowania na hydrację i doładowywanie przy scrollowaniu
const observer = new MutationObserver((mutations) => {
let shouldProcess = false;
// Sprawdzamy czy modyfikacja DOM polegała na dodaniu nowych węzłów
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldProcess = true;
break;
}
}
if (shouldProcess) {
processElements();
}
});
// Podpinamy obserwatora pod całe <body>
observer.observe(document.body, {
childList: true,
subtree: true
});
})();