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();