Onche Topic Preview

Prévisualise le premier message d'un topic au survol sur Onche.org

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Onche Topic Preview
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Prévisualise le premier message d'un topic au survol sur Onche.org
// @author       Bkz1
// @match        https://onche.org/*
// @license MIT
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Cache pour stocker les éléments de messages pré-chargés (URL normalisée -> Element cloné)
    const previewCache = new Map();

    // Variables de contrôle du survol
    let hoverTimeout = null;
    let activeLink = null;
    let mouseX = 0;
    let mouseY = 0;

    // Création et injection du style CSS personnalisé
    const style = document.createElement('style');
    style.textContent = `
        #onche-preview-tooltip {
            position: fixed;
            z-index: 999999;
            max-width: 600px;
            max-height: 400px;
            background: rgba(30, 39, 58, 0.96);
            border: 1px solid rgba(255, 255, 255, 0.12);
            border-radius: 12px;
            box-shadow: 0 12px 36px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05);
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
            overflow-y: auto;
            padding: 16px;
            pointer-events: none;
            opacity: 0;
            visibility: hidden;
            transform: translateY(12px) scale(0.98);
            transition: opacity 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                        transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),
                        visibility 0.2s;
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            box-sizing: border-box;
        }

        #onche-preview-tooltip.visible {
            opacity: 1;
            visibility: visible;
            transform: translateY(0) scale(1);
        }

        /* Réinitialisation et harmonisation de la boîte de message native */
        #onche-preview-tooltip .message {
            border: none !important;
            background: transparent !important;
            padding: 0 !important;
            margin: 0 !important;
            box-shadow: none !important;
            display: flex !important;
            flex-direction: column !important;
        }

        #onche-preview-tooltip .message-top {
            border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important;
            padding-bottom: 10px !important;
            margin-bottom: 10px !important;
            display: flex !important;
            align-items: center !important;
            gap: 12px !important;
        }

        #onche-preview-tooltip .message-infos {
            display: flex !important;
            flex-direction: column !important;
            gap: 2px !important;
        }

        #onche-preview-tooltip .message-content {
            font-size: 14.5px !important;
            line-height: 1.6 !important;
            color: #e2e8f0 !important;
            word-break: break-word !important;
        }

        #onche-preview-tooltip .avatar {
            width: 44px !important;
            height: 44px !important;
            border-radius: 50% !important;
            object-fit: cover !important;
        }

        #onche-preview-tooltip .message-username {
            font-size: 14.5px !important;
            font-weight: 600 !important;
            color: #3b82f6 !important;
            text-decoration: none !important;
        }

        #onche-preview-tooltip .message-bottom {
            margin-top: 12px !important;
            padding-top: 8px !important;
            border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
            display: flex !important;
            justify-content: flex-end !important;
        }

        #onche-preview-tooltip .message-date {
            font-size: 12px !important;
            color: #64748b !important;
        }

        #onche-preview-tooltip .message-content img,
        #onche-preview-tooltip .message-content video {
            max-width: 100% !important;
            height: auto !important;
            border-radius: 6px !important;
            margin-top: 6px !important;
        }

        #onche-preview-tooltip .message-badges img {
            width: 18px !important;
            height: 18px !important;
            max-width: 18px !important;
            max-height: 18px !important;
            margin: 0 2px !important;
            display: inline-block !important;
            vertical-align: middle !important;
        }

        /* Personnalisation de la barre de défilement de l'aperçu */
        #onche-preview-tooltip::-webkit-scrollbar {
            width: 6px;
            height: 6px;
        }
        #onche-preview-tooltip::-webkit-scrollbar-track {
            background: transparent;
        }
        #onche-preview-tooltip::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, 0.15);
            border-radius: 4px;
        }
        #onche-preview-tooltip::-webkit-scrollbar-thumb:hover {
            background: rgba(255, 255, 255, 0.3);
        }

        /* Spinner de chargement */
        .onche-preview-loader {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 24px;
            color: #94a3b8;
            font-size: 13.5px;
            font-weight: 500;
        }

        .onche-preview-spinner {
            width: 28px;
            height: 28px;
            border: 3px solid rgba(255, 255, 255, 0.08);
            border-top-color: #3b82f6;
            border-radius: 50%;
            animation: onche-spin 0.8s linear infinite;
            margin-bottom: 12px;
        }

        @keyframes onche-spin {
            to { transform: rotate(360deg); }
        }

        /* Affichage des erreurs */
        .onche-preview-error {
            display: flex;
            align-items: center;
            gap: 10px;
            color: #ef4444;
            font-size: 14px;
            padding: 12px;
            border-radius: 8px;
            background: rgba(239, 68, 68, 0.1);
            border: 1px solid rgba(239, 68, 68, 0.2);
        }
    `;
    document.head.appendChild(style);

    // Création et injection du conteneur du tooltip de prévisualisation
    const tooltip = document.createElement('div');
    tooltip.id = 'onche-preview-tooltip';
    document.body.appendChild(tooltip);

    // Normalise l'URL d'un topic pour cibler la page 1 et sans ancre/paramètre
    function getPage1Url(href) {
        try {
            const url = new URL(href, window.location.origin);
            url.hash = '';
            url.search = '';

            const pathname = url.pathname;
            // Vérifie si c'est un lien vers un topic
            if (!pathname.startsWith('/topic/')) return null;

            const segments = pathname.split('/').filter(Boolean);

            // Format typique : ["topic", "1196595", "slug", "page"]
            if (segments.length >= 3) {
                const lastSegment = segments[segments.length - 1];
                if (/^\d+$/.test(lastSegment)) {
                    // Supprime le segment du numéro de page
                    segments.pop();
                }
                url.pathname = '/' + segments.join('/');
            }
            return url.href;
        } catch (e) {
            return null;
        }
    }

    // Convertit les URLs relatives d'un élément cloné en URLs absolues
    function resolveRelativeUrls(element, baseUrl) {
        const attributes = ['src', 'href'];
        attributes.forEach(attr => {
            element.querySelectorAll(`[${attr}]`).forEach(el => {
                const val = el.getAttribute(attr);
                if (val) {
                    try {
                        el.setAttribute(attr, new URL(val, baseUrl).href);
                    } catch (e) {}
                }
            });
        });
    }

    // Cache et affiche le tooltip
    function hideTooltip() {
        tooltip.classList.remove('visible');
    }

    function showTooltipLoading() {
        tooltip.innerHTML = `
            <div class="onche-preview-loader">
                <div class="onche-preview-spinner"></div>
                <span>Chargement de l'aperçu...</span>
            </div>
        `;
        tooltip.classList.add('visible');
    }

    function showTooltipContent(contentNode) {
        tooltip.innerHTML = '';
        tooltip.appendChild(contentNode.cloneNode(true));
        tooltip.classList.add('visible');
    }

    function showTooltipError(message) {
        tooltip.innerHTML = `
            <div class="onche-preview-error">
                <span style="font-size: 18px; line-height: 1;">⚠️</span>
                <span>${message}</span>
            </div>
        `;
        tooltip.classList.add('visible');
    }

    // Met à jour la position du tooltip en évitant qu'il sorte de l'écran
    function updateTooltipPosition(x, y) {
        const offset = 18;
        let targetX = x + offset;
        let targetY = y + offset;

        const tooltipWidth = tooltip.offsetWidth;
        const tooltipHeight = tooltip.offsetHeight;

        const screenWidth = window.innerWidth;
        const screenHeight = window.innerHeight;

        // Ajustement horizontal si débordement à droite
        if (targetX + tooltipWidth > screenWidth) {
            targetX = x - tooltipWidth - offset;
        }
        if (targetX < 10) {
            targetX = 10;
        }

        // Ajustement vertical si débordement en bas
        if (targetY + tooltipHeight > screenHeight) {
            targetY = y - tooltipHeight - offset;
        }
        if (targetY < 10) {
            targetY = 10;
        }

        tooltip.style.left = `${targetX}px`;
        tooltip.style.top = `${targetY}px`;
    }

    // Déclenche le chargement et l'affichage de l'aperçu d'un topic
    function showPreviewForLink(link) {
        const page1Url = getPage1Url(link.href);
        if (!page1Url) return;

        showTooltipLoading();
        updateTooltipPosition(mouseX, mouseY);

        // Si l'aperçu est déjà en cache
        if (previewCache.has(page1Url)) {
            showTooltipContent(previewCache.get(page1Url));
            return;
        }

        // Chargement asynchrone de la première page du topic
        fetch(page1Url)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`Erreur réseau (${response.status})`);
                }
                return response.text();
            })
            .then(html => {
                // S'assurer que le curseur est toujours sur ce lien
                if (activeLink !== link) return;

                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                const firstMessage = doc.querySelector('.message[data-is-first]') || doc.querySelector('.message');

                if (!firstMessage) {
                    throw new Error("Impossible de lire le contenu de ce sujet.");
                }

                // Cloner et nettoyer l'élément du premier message
                const previewEl = firstMessage.cloneNode(true);

                // Résoudre les liens et médias relatifs
                resolveRelativeUrls(previewEl, page1Url);

                // Retirer les éléments indésirables (signature, publicités, boutons)
                const signature = previewEl.querySelector('.signature');
                if (signature) signature.remove();

                const actions = previewEl.querySelector('.message-actions, .right');
                if (actions) actions.remove();

                const ads = previewEl.querySelector('.topic-ad');
                if (ads) ads.remove();

                // Stocker dans le cache local
                previewCache.set(page1Url, previewEl);

                showTooltipContent(previewEl);
            })
            .catch(err => {
                if (activeLink !== link) return;
                showTooltipError(err.message);
            });
    }

    // Gestion du cycle de survol avec temporisation
    function clearHover() {
        if (hoverTimeout) {
            clearTimeout(hoverTimeout);
            hoverTimeout = null;
        }
        activeLink = null;
        hideTooltip();
    }

    // Écouteur global pour suivre la souris
    document.addEventListener('mousemove', (e) => {
        mouseX = e.clientX;
        mouseY = e.clientY;
        if (tooltip.classList.contains('visible')) {
            updateTooltipPosition(mouseX, mouseY);
        }
    });

    // Délégation d'événements pour le survol des titres de topics
    document.addEventListener('mouseover', (e) => {
        const link = e.target.closest('a.topic-subject.link');
        if (!link) return;

        if (activeLink !== link) {
            clearHover();
            activeLink = link;
            hoverTimeout = setTimeout(() => {
                showPreviewForLink(link);
            }, 350); // Délai d'attente de 350ms
        }
    });

    document.addEventListener('mouseout', (e) => {
        if (!activeLink) return;

        // Vérifie si le pointeur quitte vraiment l'élément survolé
        const related = e.relatedTarget;
        if (!related || !activeLink.contains(related)) {
            clearHover();
        }
    });

})();