diff --git a/tampermonkey/hn_url_highlighter_with_preview.js b/tampermonkey/hn_url_highlighter_with_preview.js index 0a817e0..21f04f9 100644 --- a/tampermonkey/hn_url_highlighter_with_preview.js +++ b/tampermonkey/hn_url_highlighter_with_preview.js @@ -1,8 +1,8 @@ // ==UserScript== -// @name Hacker News URL Highlighter with Enhanced Panel and Scores +// @name Hacker News URL Highlighter with Enhanced Panel and Scores & Comments // @namespace https://news.ycombinator.com/ -// @version 1.5 -// @description Highlights URLs in comments on Hacker News, lists them in a toggleable panel with sorting, color coding based on frequency or upvotes, and displays scores next to each URL. Provides export functionality and hover previews only in comments. +// @version 1.6 +// @description Highlights URLs in comments, displays them in a toggleable panel with sorting (frequency, name, upvotes, comment count). // @author MorganGeek // @match https://news.ycombinator.com/item?id=* // @grant GM_xmlhttpRequest @@ -11,267 +11,175 @@ // @run-at document-end // ==/UserScript== -(function () { - 'use strict'; +(function() { + 'use strict'; - // Configuration des couleurs en fonction des fréquences ou des upvotes - const COLORS = { - low: '#d1e7dd', // Vert pâle - medium: '#fff3cd', // Jaune pâle - high: '#f8d7da' // Rouge pâle - }; + const COLORS={low:'#d1e7dd',medium:'#fff3cd',high:'#f8d7da'}; + const toggleButton=document.createElement('button'); + toggleButton.textContent='Afficher les URLs'; + Object.assign(toggleButton.style,{ + position:'fixed',bottom:'10px',right:'10px',zIndex:1000,padding:'10px 15px', + backgroundColor:'#ff6600',color:'#fff',border:'none',borderRadius:'5px',cursor:'pointer' + }); + document.body.appendChild(toggleButton); - // Créer un bouton pour basculer l'affichage du panneau - const toggleButton = document.createElement('button'); - toggleButton.textContent = 'Afficher les URLs'; - toggleButton.style.position = 'fixed'; - toggleButton.style.bottom = '10px'; - toggleButton.style.right = '10px'; - toggleButton.style.zIndex = 1000; - toggleButton.style.padding = '10px 15px'; - toggleButton.style.backgroundColor = '#ff6600'; - toggleButton.style.color = '#fff'; - toggleButton.style.border = 'none'; - toggleButton.style.borderRadius = '5px'; - toggleButton.style.cursor = 'pointer'; - document.body.appendChild(toggleButton); + const panel=document.createElement('div'); + panel.id='url-panel'; + Object.assign(panel.style,{ + position:'fixed',top:'10px',right:'10px',width:'350px',backgroundColor:'#fff', + border:'1px solid #ccc',borderRadius:'5px',padding:'10px',boxShadow:'0 0 10px rgba(0,0,0,.2)', + maxHeight:'80vh',overflowY:'auto',zIndex:1000,display:'none' + }); + panel.innerHTML=` +

URLs

+
+ + +
+ +
+ + +
+ + `; + document.body.appendChild(panel); - // Créer un panneau flottant pour afficher les URLs (caché par défaut) - const panel = document.createElement('div'); - panel.id = 'url-panel'; - panel.style.position = 'fixed'; - panel.style.top = '10px'; - panel.style.right = '10px'; - panel.style.width = '350px'; - panel.style.backgroundColor = '#fff'; - panel.style.border = '1px solid #ccc'; - panel.style.borderRadius = '5px'; - panel.style.padding = '10px'; - panel.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.2)'; - panel.style.maxHeight = '80vh'; - panel.style.overflowY = 'auto'; - panel.style.zIndex = 1000; - panel.style.display = 'none'; // Caché par défaut - panel.innerHTML = ` -

URLs

-
- - -
- -
- - -
- - `; - document.body.appendChild(panel); + const tooltip=document.createElement('div'); + tooltip.id='url-tooltip'; + Object.assign(tooltip.style,{ + position:'absolute',background:'#fff',border:'1px solid #ccc',borderRadius:'5px', + padding:'10px',boxShadow:'0 0 10px rgba(0,0,0,.2)',maxWidth:'300px', + display:'none',zIndex:1001 + }); + document.body.appendChild(tooltip); - // Créer une infobulle pour les aperçus - const tooltip = document.createElement('div'); - tooltip.id = 'url-tooltip'; - tooltip.style.position = 'absolute'; - tooltip.style.backgroundColor = '#fff'; - tooltip.style.border = '1px solid #ccc'; - tooltip.style.borderRadius = '5px'; - tooltip.style.padding = '10px'; - tooltip.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.2)'; - tooltip.style.maxWidth = '300px'; - tooltip.style.display = 'none'; - tooltip.style.zIndex = 1001; - document.body.appendChild(tooltip); - - // Structure de données pour stocker les URLs avec leurs métadonnées - const urlData = {}; - - // Extraire les URLs des commentaires et les mettre en évidence - const comments = document.querySelectorAll('.commtext'); - comments.forEach(comment => { - const links = comment.querySelectorAll('a[href]'); - links.forEach(link => { - const url = link.href; - if (!urlData[url]) { - urlData[url] = { - url: url, - count: 1, - upvotes: 0 // Placeholder, à implémenter si les upvotes sont disponibles - }; - link.style.backgroundColor = '#ffff99'; // Couleur de surbrillance initiale - - // Ajouter l'URL à la liste dans le panneau - const listItem = document.createElement('li'); - listItem.innerHTML = `${url} 1`; - listItem.style.marginBottom = '5px'; - listItem.style.wordWrap = 'break-word'; - listItem.style.cursor = 'pointer'; - listItem.dataset.url = url; - panel.querySelector('#url-list').appendChild(listItem); - } else { - urlData[url].count += 1; - // Mettre à jour le compteur dans le panneau - const existingItem = panel.querySelector(`li[data-url="${CSS.escape(url)}"] span:last-child`); - if (existingItem) { - existingItem.textContent = urlData[url].count; - } - } - - // Ajouter la fonctionnalité d'aperçu au survol des liens - link.addEventListener('mouseover', () => { - GM_xmlhttpRequest({ - method: 'GET', - url: url, - onload: response => { - tooltip.innerHTML = ''; - const parser = new DOMParser(); - const doc = parser.parseFromString(response.responseText, 'text/html'); - const title = doc.querySelector('title') ? doc.querySelector('title').innerText : 'No Title'; - const description = doc.querySelector('meta[name="description"]') ? doc.querySelector('meta[name="description"]').content : 'No Description'; - - tooltip.innerHTML = `${title}

${description}

`; - tooltip.style.display = 'block'; - }, - onerror: () => { - tooltip.innerHTML = 'Preview unavailable'; - tooltip.style.display = 'block'; - } - }); - - const rect = link.getBoundingClientRect(); - tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`; - tooltip.style.left = `${rect.left + window.scrollX}px`; - }); - - link.addEventListener('mouseout', () => { - tooltip.style.display = 'none'; - }); + const urlData={}; + const comments=document.querySelectorAll('.commtext'); + comments.forEach(comment=>{ + const links=comment.querySelectorAll('a[href]'); + links.forEach(link=>{ + const u=link.href; + if(!urlData[u]) urlData[u]={url:u,count:0,upvotes:0,commentCount:0}; + urlData[u].count++; + link.style.backgroundColor='#ffff99'; + link.addEventListener('mouseover',()=>{ + GM_xmlhttpRequest({ + method:'GET',url:u, + onload:r=>{ + tooltip.innerHTML=''; + const d=new DOMParser().parseFromString(r.responseText,'text/html'); + const t=d.querySelector('title')?d.querySelector('title').innerText:'No Title'; + const desc=d.querySelector('meta[name="description"]')?d.querySelector('meta[name="description"]').content:'No Description'; + tooltip.innerHTML=`${t}

${desc}

`; + tooltip.style.display='block'; + }, + onerror:()=>{ + tooltip.innerHTML='Preview unavailable'; + tooltip.style.display='block'; + } }); + const rect=link.getBoundingClientRect(); + tooltip.style.top=`${rect.bottom+window.scrollY+5}px`; + tooltip.style.left=`${rect.left+window.scrollX}px`; + }); + link.addEventListener('mouseout',()=>{tooltip.style.display='none';}); }); + }); - // Fonction pour mettre à jour l'affichage du panneau selon les filtres - function updatePanel() { - const sortBy = panel.querySelector('#sort-select').value; - const limit = panel.querySelector('#limit-select').value; + function updatePanel(){ + const sortBy=panel.querySelector('#sort-select').value; + const limit=panel.querySelector('#limit-select').value; + let arr=Object.values(urlData); + if(sortBy==='frequency') arr.sort((a,b)=>b.count-a.count); + else if(sortBy==='name') arr.sort((a,b)=>a.url.localeCompare(b.url)); + else if(sortBy==='upvotes') arr.sort((a,b)=>b.upvotes-a.upvotes); + else if(sortBy==='commentaires') arr.sort((a,b)=>b.commentCount-a.commentCount); + if(limit!=='all') arr=arr.slice(0,parseInt(limit)); + const urlList=panel.querySelector('#url-list'); + urlList.innerHTML=''; + const counts=arr.map(i=>i.count); + const maxC=Math.max(...counts),minC=Math.min(...counts); + arr.forEach(item=>{ + const li=document.createElement('li'); + li.style.marginBottom='5px';li.style.wordWrap='break-word';li.style.cursor='pointer'; + li.dataset.url=item.url; + const r=(item.count-minC)/(maxC-minC+1); + li.style.backgroundColor=r>0.66?COLORS.high:r>0.33?COLORS.medium:COLORS.low; + li.innerHTML=` + ${item.url} + + ${item.count}${item.upvotes>0?` / ${item.upvotes}`:''}${item.commentCount>0?` / ${item.commentCount}`:''} + + `; + li.addEventListener('click',()=>{ + GM_setClipboard(item.url,'text'); + alert('URL copiée.'); + }); + urlList.appendChild(li); + }); + } - // Convertir l'objet en tableau pour le tri - let urlsArray = Object.values(urlData); + updatePanel(); + panel.querySelector('#sort-select').addEventListener('change',updatePanel); + panel.querySelector('#limit-select').addEventListener('change',updatePanel); + toggleButton.addEventListener('click',()=>{ + if(panel.style.display==='none'){panel.style.display='block';toggleButton.textContent='Masquer les URLs';} + else{panel.style.display='none';toggleButton.textContent='Afficher les URLs';} + }); + panel.querySelector('#copy-urls').addEventListener('click',()=>{ + GM_setClipboard(Object.keys(urlData).join('\n'),'text'); + alert('URLs copiées.'); + }); - // Trier en fonction du critère sélectionné - if (sortBy === 'frequency') { - urlsArray.sort((a, b) => b.count - a.count); - } else if (sortBy === 'name') { - urlsArray.sort((a, b) => a.url.localeCompare(b.url)); - } else if (sortBy === 'upvotes') { - urlsArray.sort((a, b) => b.upvotes - a.upvotes); - } - - // Appliquer la limite si nécessaire - if (limit !== 'all') { - const limitNumber = parseInt(limit); - urlsArray = urlsArray.slice(0, limitNumber); - } - - const urlList = panel.querySelector('#url-list'); - urlList.innerHTML = ''; // Vider la liste actuelle - - // Déterminer les fréquences pour la coloration - const counts = urlsArray.map(item => item.count); - const maxCount = Math.max(...counts); - const minCount = Math.min(...counts); - - urlsArray.forEach(item => { - const listItem = document.createElement('li'); - listItem.style.marginBottom = '5px'; - listItem.style.wordWrap = 'break-word'; - listItem.style.cursor = 'pointer'; - listItem.dataset.url = item.url; - - // Définir la couleur en fonction de la fréquence - let color; - const ratio = (item.count - minCount) / (maxCount - minCount + 1); - if (ratio > 0.66) { - color = COLORS.high; - } else if (ratio > 0.33) { - color = COLORS.medium; - } else { - color = COLORS.low; - } - listItem.style.backgroundColor = color; - - // Créer le contenu avec URL et score - listItem.innerHTML = ` - ${item.url} - ${item.count}${item.upvotes > 0 ? ` / ${item.upvotes}` : ''} - `; - - // Ajouter l'événement de clic pour copier l'URL - listItem.addEventListener('click', () => { - GM_setClipboard(item.url, 'text'); - alert('URL copiée dans le presse-papiers!'); - }); - - urlList.appendChild(listItem); - }); + const commentRows=document.querySelectorAll('tr.comtr'); + commentRows.forEach(row=>{ + const commentLink=row.querySelector('a[href^="item?id="]'); + if(commentLink){ + const pointsSpan=row.querySelector('.score'); + const pts=pointsSpan?parseInt(pointsSpan.textContent):0; + const c=row.querySelector('.commtext'); + if(c){ + const links=c.querySelectorAll('a[href]'); + links.forEach(l=>{if(urlData[l.href]) urlData[l.href].upvotes+=pts;}); + } } + }); - // Initialiser le panneau - updatePanel(); - - // Écouter les changements dans les sélecteurs de tri et de limite - panel.querySelector('#sort-select').addEventListener('change', updatePanel); - panel.querySelector('#limit-select').addEventListener('change', updatePanel); - - // Gérer le bouton de bascule pour afficher/masquer le panneau - toggleButton.addEventListener('click', () => { - if (panel.style.display === 'none') { - panel.style.display = 'block'; - toggleButton.textContent = 'Masquer les URLs'; - } else { - panel.style.display = 'none'; - toggleButton.textContent = 'Afficher les URLs'; - } - }); - - // Gérer le bouton "Copier les URLs" - panel.querySelector('#copy-urls').addEventListener('click', () => { - const urlArray = Object.keys(urlData); - GM_setClipboard(urlArray.join('\n'), 'text'); - alert('URLs copiées dans le presse-papiers!'); - }); - - // Optionnel : Collecter les upvotes des commentaires pour chaque URL - // Note : Hacker News n'expose pas directement les upvotes des commentaires via le DOM. - // Si vous avez une méthode pour obtenir les upvotes, vous pouvez l'implémenter ici. - // Par exemple, si les points des commentaires sont visibles, vous pouvez les extraire et les attribuer aux URLs. - - // Exemple de récupération des points des commentaires (si disponibles) - const commentRows = document.querySelectorAll('tr.comtr'); - commentRows.forEach(row => { - const commentLink = row.querySelector('a[href^="item?id="]'); - if (commentLink) { - const pointsSpan = row.querySelector('.score'); - const points = pointsSpan ? parseInt(pointsSpan.textContent) : 0; - const comment = row.querySelector('.commtext'); - if (comment) { - const links = comment.querySelectorAll('a[href]'); - links.forEach(link => { - const url = link.href; - if (urlData[url]) { - urlData[url].upvotes += points; - } - }); - } - } - }); - - // Mettre à jour le panneau après avoir collecté les upvotes - updatePanel(); + const childrenMap={}; + commentRows.forEach(r=>{ + const id=r.id;const pid=r.getAttribute('data-parent'); + if(!pid||!id) return; + if(!childrenMap[pid]) childrenMap[pid]=[]; + childrenMap[pid].push(id); + }); + const cache={}; + function size(id){ + if(!childrenMap[id]) return 0; + if(cache[id]!=null) return cache[id]; + let s=childrenMap[id].length; + childrenMap[id].forEach(k=>{s+=size(k);}); + cache[id]=s;return s; + } + commentRows.forEach(r=>{ + const id=r.id; + const c=r.querySelector('.commtext'); + if(!id||!c) return; + const count=size(id); + const links=c.querySelectorAll('a[href]'); + links.forEach(l=>{if(urlData[l.href]) urlData[l.href].commentCount+=count;}); + }); + updatePanel(); })();