OP Only 2026

Filtre automatiquement un topic pour ne garder que les messages de l'auteur. Version adaptée à la MAJ de jvc de 2026. Code en partie repris de https://jvscript.fr/script/jvchat-premium (JVChat Premium FORK by Rand0max).

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

Advertisement:

// ==UserScript==
// @name         OP Only 2026
// @namespace    OP Only 2026
// @author       navet-navrant
// @description  Filtre automatiquement un topic pour ne garder que les messages de l'auteur. Version adaptée à la MAJ de jvc de 2026. Code en partie repris de https://jvscript.fr/script/jvchat-premium (JVChat Premium FORK by Rand0max).
// @version      1.0.0
// @license      MIT
// @match        https://www.jeuxvideo.com/forums/42-*
// ==/UserScript==

// Délai d'attente entre le chargement de deux pages (ms)
const DELAY = 2500;

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function parseURL(url) {
    const regex = /^(.*?)(\/\d+-\d+-\d+-)(\d+)(-\d+-\d+-\d+-)(.*?)(\.htm)(.*)$/i;
    const [_, domain, ids, page, nums, title, htm, anchor] = url.match(regex);

    return { domain: domain, ids: ids, page: page, nums: nums, title: title, htm: htm, anchor: anchor };
}

function buildURL(dict) {
    return `${dict.domain}${dict.ids}${dict.page}${dict.nums}${dict.title}${dict.htm}${dict.anchor}`;
}

function jvCake(cls) {
    const base16 = '0A12B34C56D78E9F';
    const s = cls.split(' ')[1];
    let link = '';

    for (let i = 0; i < s.length; i += 2) {
        link += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1)));
    }

    return link;
}

function decodeJvCare(doc) {
    const jvcares = doc.getElementsByClassName('JvCare');

    for (let i = jvcares.length - 1; i >= 0; i--) {
        const jvcare = jvcares[i];
        const a = doc.createElement('a');
        a.setAttribute('target', '_blank');
        a.setAttribute('href', jvCake(jvcare.getAttribute('class')));
        a.innerHTML = jvcare.innerHTML;
        const otherClasses = jvcare.getAttribute('class').split(' ').slice(2).join(' ');
        if (otherClasses) a.classList.add(...otherClasses.split(' ').filter(Boolean));
        jvcare.replaceWith(a);
    }
}

function getOPPseudo(doc) {
    return doc.querySelector('#listMessages .messageUser__label')?.textContent.trim();
}

function getLastPage(document) {
    const spans = document.querySelectorAll('.pagination__item, .pagination__button, .pagination__navigation a, .pagination__navigation span');
    let lastPage = 1;

    for (let span of spans) {
        const txt = span.textContent.trim();
        if (/^\d+$/.test(txt)) {
            const page = parseInt(txt);
            if (page > lastPage) {
                lastPage = page;
            }
        }
    }

    return lastPage;
}

async function fetchPage(dict) {
    const res = await fetch(buildURL(dict));
    const html = await res.text();
    const doc = new DOMParser().parseFromString(html, 'text/html');
    decodeJvCare(doc);
    return doc;
}

function expandNestedQuotes(msgEl) {
    msgEl.querySelectorAll('.message__blockquote').forEach(bq => {
        bq.classList.add('message__blockquote--visible');
    });
    msgEl.querySelectorAll('.message__collapsedQuote').forEach(btn => {
        btn.remove();
    });
}

function addMessageLink(msgEl, pageDict) {
    const msgId = msgEl.id?.replace('message-', '');
    if (!msgId) return;

    const anchoredDict = { ...pageDict, anchor: `#post_${msgId}` };
    const messageURL = buildURL(anchoredDict);

    const linkIcon = document.createElement('a');
    linkIcon.href = messageURL;
    linkIcon.target = '_blank';
    linkIcon.rel = 'noopener noreferrer';
    linkIcon.title = 'Voir le message dans son contexte';
    linkIcon.className = 'oponly-message-link';
    linkIcon.textContent = '↗';

    const card = msgEl.querySelector('.messageUser__card') || msgEl;
    card.appendChild(linkIcon);
}

function restoreLinks(msgEl) {
    msgEl.querySelectorAll('a').forEach(a => {
        if (a.classList.contains('messageUser__label') ||
            a.classList.contains('messageUser__date') ||
            a.classList.contains('messageUser__dateEditLink') ||
            a.classList.contains('avatar')) {
            return;
        }

        a.style.pointerEvents = 'auto';
        a.style.cursor = 'pointer';
        if (!a.querySelector('img')) {
            a.style.color = '#3690e0';
        }
    });
}

function addOPMessages(doc, OP, container, pageDict) {
    let count = 0;
    doc.querySelectorAll('#listMessages .messageUser').forEach(msg => {
        const pseudo = msg.querySelector('.messageUser__label')?.textContent.trim();
        if (pseudo === OP) {
            const clone = msg.cloneNode(true);
            clone.querySelector('.messageUser__headerActions')?.remove();
            expandNestedQuotes(clone);
            restoreLinks(clone);
            addMessageLink(clone, pageDict);
            container.appendChild(clone);
            count++;
        }
    });
    return count;
}

function showStatus(text, loading = true) {
    const paginationTop = document.querySelector('.js-pagination-top');
    const paginationBot = document.querySelector('.container__pagination:not(.js-pagination-top)');

    let statusEl = document.querySelector('#oponly-status');
    if (!statusEl) {
        if (paginationTop) paginationTop.style.display = 'none';
        if (paginationBot) paginationBot.style.display = 'none';

        const container = document.querySelector('#listMessages');
        if (!container) return;
        container.insertAdjacentHTML('beforebegin', `<div id='oponly-status' style='padding: 0.5rem 1rem; font-weight: bold;'></div>`);
        statusEl = document.querySelector('#oponly-status');
    }

    statusEl.innerHTML = loading
        ? `<span class='oponly-spinner'></span><span>${text}</span>`
        : `<span>${text}</span>`;
}

async function triggerOpOnly(event) {
    const clickedBtn = event.currentTarget;
    clickedBtn.classList.add('btn-oponly--activated');
    clickedBtn.disabled = true;

    try {
        let topicUrl = document.URL;
        let urlToFetch = parseURL(topicUrl);
        urlToFetch.page = 1;
        urlToFetch.anchor = '';

        const container = document.querySelector('#listMessages');
        if (!container) return;
        container.innerHTML = '';

        showStatus('Chargement page 1...');
        const firstPageDoc = await fetchPage(urlToFetch);
        const OP = getOPPseudo(firstPageDoc);
        if (!OP) {
            showStatus('Impossible de récupérer l\'auteur du topic.', false);
            clickedBtn.classList.remove('btn-oponly--activated');
            clickedBtn.disabled = false;
            return;
        }
        const lastPage = getLastPage(firstPageDoc);

        let opMessageCount = addOPMessages(firstPageDoc, OP, container, urlToFetch);
        showStatus(`1 sur ${lastPage} pages chargées - ${opMessageCount} messages de l'OP récupérés`, lastPage > 1);

        for (let page = 2; page <= lastPage; page++) {
            await sleep(DELAY);
            urlToFetch.page = page;
            const doc = await fetchPage(urlToFetch);
            opMessageCount += addOPMessages(doc, OP, container, urlToFetch);
            const isLast = page === lastPage;
            showStatus(`${page} sur ${lastPage} pages chargées - ${opMessageCount} messages de l'OP récupérés`, !isLast);
        }
    } catch (e) {
        alert(`OPOnly error : ${e.message}`);
        console.error('OPOnly error : ', e.message);
        clickedBtn.classList.remove('btn-oponly--activated');
        clickedBtn.disabled = false;
    }
}

function initializeButton() {
    let css = `<style type='text/css'>
    .btn-oponly.buttonsNavbar__button {
        font-weight: bold;
    }
    .btn-oponly.buttonsNavbar__button.btn-oponly--activated {
        color: #f66031;
        cursor: not-allowed;
    }
    #oponly-status {
        display: flex;
        align-items: center;
        gap: 0.5rem;
    }
    .oponly-spinner {
        width: 14px;
        height: 14px;
        border: 2px solid #ccc;
        border-top-color: #ff6333;
        border-radius: 50%;
        animation: oponly-spin 0.7s linear infinite;
    }
    @keyframes oponly-spin {
        to { transform: rotate(360deg); }
    }
    .messageUser__card {
        position: relative;
    }
    .oponly-message-link {
        position: absolute;
        top: 1rem;
        right: 1rem;
        text-decoration: none;
        cursor: pointer;
        font-size: 1.5rem;
        opacity: 0.6;
    }
    .oponly-message-link:hover {
        opacity: 1;
    }
    @media (min-width: 1000px) and (max-width: 1199px) {
        .buttonsNavbar {
            padding-left: 0;
            padding-right: 0;
        }
        .icon-refresh + .buttonsNavbar__label {
            display : none;
        }
    }
    </style>`

    document.head.insertAdjacentHTML('beforeend', css);
    const opOnlyButton = `<span class='buttonsNavbar__space'></span><button class='buttonsNavbar__button btn-oponly' type='button'><i class='buttonsNavbar__icon icon-comments'></i><div class='buttonsNavbar__label'>OP</div></button>`;
    const buttonsNavbar = document.querySelectorAll('.buttonsNavbar');

    for (let bloc of buttonsNavbar) {
        const refreshBtn = bloc.querySelector('.icon-refresh')?.closest('button');
        refreshBtn.insertAdjacentHTML('afterend', opOnlyButton);
    }

    let buttons = document.getElementsByClassName('btn-oponly');
    for (let btn of buttons) {
        btn.addEventListener('click', triggerOpOnly);
    }
}

function main() {
    if (document.querySelector('.buttonsNavbar')) {
        initializeButton();
        return;
    }

    let observer = new MutationObserver(function (mutations, obs) {
        if (document.querySelector('.buttonsNavbar')) {
            obs.disconnect();
            initializeButton();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(() => observer.disconnect(), 5000);
}

main();