Prévisualise le premier message d'un topic au survol sur Onche.org
// ==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();
}
});
})();