Hacker News Tweaks (Modern)

Improve Hacker News readability with larger fonts, modern styling.

Version au 21/02/2025. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey 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 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         Hacker News Tweaks (Modern)
// @namespace    HackerNewsReadabilityTweaks
// @homepage     https://github.com/Meekelis/Hacker-News-ReadabilityTweaks
// @version      1.0
// @description  Improve Hacker News readability with larger fonts, modern styling.
// @match        *://news.ycombinator.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ycombinator.com
// @license      MIT
// @grant        none
// ==/UserScript==
(function() {
  'use strict';
  const css = `
    .comment-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0.5em 0;
      border-bottom: 1px solid #e1e1e1;
      background: #f9f9f9;
    }
    .comment-header__details {
      display: flex;
      gap: 0.5em;
      align-items: center;
    }
    .comment-header__username {
      font-weight: bold;
      color: #333;
      text-decoration: none;
    }
    .comment-header__timestamp a {
      color: #888;
      text-decoration: none;
    }
    .comment-header__actions {
      display: flex;
      gap: 0.5em;
      align-items: center;
    }
    .comment-header__link {
      font-size: 0.9em;
      color: #0077cc;
      text-decoration: none;
    }
    .comment-header__toggle {
      display: inline-block;
      width: 2em;
      text-align: center;
      background: none;
      border: none;
      color: #0077cc;
      cursor: pointer;
      font-size: 0.9em;
      line-height: 1;
    }
  `;
  const styleEl = document.createElement('style');
  styleEl.textContent = css;
  document.head.appendChild(styleEl);
  document.querySelectorAll('.comhead').forEach(comhead => {
    const origToggle = comhead.querySelector('a.togg');
    if (origToggle) { origToggle.style.display = 'none'; }
    const userEl = comhead.querySelector('a.hnuser');
    if (!userEl) return;
    const username = userEl.textContent.trim();
    const userHref = userEl.href;
    const ageEl = comhead.querySelector('.age');
    const ageLink = ageEl ? ageEl.querySelector('a') : null;
    const timestamp = ageLink ? ageLink.textContent.replace(/^on\s+/i, '').trim() : '';
    const timeHref = ageLink ? ageLink.href : '#';
    const datetime = ageEl ? ageEl.getAttribute('title') : '';
    let nextHref = '#';
    const navs = comhead.querySelector('.navs');
    if (navs) {
      const nextLink = Array.from(navs.querySelectorAll('a')).find(a => /next/i.test(a.textContent));
      if (nextLink) { nextHref = nextLink.href; }
    }
    const toggleId = origToggle ? origToggle.id : 'toggle';
    const newHeader = document.createElement('div');
    newHeader.className = 'comment-header';
    newHeader.innerHTML = `
      <div class="comment-header__details">
        <a href="${userHref}" class="comment-header__username" target="_blank" rel="noopener noreferrer">
          ${username}
        </a>
        <time datetime="${datetime}" class="comment-header__timestamp">
          <a href="${timeHref}" rel="noopener noreferrer">
            ${timestamp}
          </a>
        </time>
      </div>
      <div class="comment-header__actions">
        <a href="${nextHref}" class="comment-header__link" rel="noopener noreferrer">
          Next
        </a>
        <button class="comment-header__toggle" id="new-toggle-${toggleId}" aria-expanded="true">–</button>
      </div>
    `;
    comhead.innerHTML = '';
    comhead.appendChild(newHeader);
    const defaultCell = comhead.closest('td.default');
    if (!defaultCell) return;
    const commentText = defaultCell.querySelector('.comment');
    if (!commentText) return;
    const collapseBtn = newHeader.querySelector('.comment-header__toggle');
    collapseBtn.addEventListener('click', function(event) {
      event.preventDefault();
      const button = event.target;
      const commentRow = button.closest('.athing.comtr');
      if (!commentRow) return;
      const isCollapsed = button.textContent === '+';
      let nextSibling = commentRow.nextElementSibling;
      while (nextSibling && nextSibling.classList.contains('athing') && nextSibling.classList.contains('comtr')) {
        const indentOfSibling = parseInt(nextSibling.querySelector('.ind').getAttribute('indent') || '0', 10);
        const indentOfCurrent = parseInt(commentRow.querySelector('.ind').getAttribute('indent') || '0', 10);
        if (indentOfSibling > indentOfCurrent) {
          nextSibling.style.display = isCollapsed ? '' : 'none';
        } else { break; }
        nextSibling = nextSibling.nextElementSibling;
      }
      button.textContent = isCollapsed ? '–' : '+';
      button.setAttribute('aria-expanded', (!isCollapsed).toString());
    });
  });
})();
(function(){
  'use strict';
  const style1 = `
  <style>
    :root {
      --accent: #ff6600;
      --accent-light: rgba(255,102,0,0.1);
      --spacing: 0.5rem;
      --radius: 8px;
      --grey: #757575;
      --bg: #f9f9f9;
      --font-main: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
    }
    html, body, td, .title, .comment, .default { font-family: var(--font-main); }
    html, body { margin: 0; padding: 0; }
    body, td, .title, .pagetop, .comment { font-size: 1rem; line-height: 1.5; }
    .votelinks, html[op='news'] .title { vertical-align: inherit; }
    .comment-tree .votelinks,
    html[op='threads'] .votelinks,
    html[op='newcomments'] .votelinks { vertical-align: top; }
    span.titleline { font-size: 1rem; line-height: 1.2; margin: var(--spacing) 0; display: block; font-weight: bold; }
    html[op='item'] span.titleline { font-size: 1.0rem; }
    html[op='news'] #hnmain>tbody>tr:nth-child(3)>td>table,
    html[op='newest'] #hnmain>tbody>tr:nth-child(3)>td>table,
    html[op='ask'] #hnmain>tbody>tr:nth-child(3)>td>table,
    html[op='newcomments'] #hnmain>tbody>tr:nth-child(3)>td>table,
    html[op='shownew'] #hnmain>tbody>tr:nth-child(3)>td>table,
    html[op='submitted'] #hnmain>tbody>tr:nth-child(3)>td>table,
    html[op='favorites'] #hnmain>tbody>tr:nth-child(3)>td>table:nth-child(2),
    html[op='front'] #hnmain>tbody>tr:nth-child(3)>td>table:nth-child(2),
    html[op='show'] #hnmain>tbody>tr:nth-child(3)>td>table:nth-child(2) { margin-left: var(--spacing); }
    .sitebit.comhead { margin-left: var(--spacing); }
    .subtext, .subline { font-size: 0.75rem; }
    #hnmain { background: var(--bg); }
    #hnmain>tbody>tr:first-child>td { padding: var(--spacing); }
    #hnmain>tbody>tr:first-child>td>table>tbody>tr:first-child>td { padding-right: var(--spacing)!important; }
    .hnname { font-size: 1.4em; font-weight: bold; }
    .comment, .toptext { max-width: 40em; margin: auto; }
    .toptext, a, a:visited { color: #333; text-decoration: none; transition: color 0.2s; }
    input { padding: var(--spacing); }
    input, textarea { background: white; border: 1px solid var(--grey); padding: 6px; border-radius: var(--radius); transition: box-shadow 0.2s; }
    input:focus, textarea:focus { box-shadow: 0 0 5px var(--accent); }
    input[type="text"]:hover { text-decoration: none; }
    input[type='button'] { cursor: pointer; }
    .downvoted .commtext { color: var(--grey); }
    .quote { margin: 16px; border: 1px solid var(--accent-light); border-left: 6px solid var(--accent); padding: 16px; color: var(--grey); background: var(--accent-light); border-radius: var(--radius); font-size: 0.9em; }
    .hidden { display: none; }
    .showComment a, .hideComment, .hideComment:link, .hideComment:visited { color: var(--accent); text-decoration: underline; }
    .hideComment { margin-left: var(--spacing); }
    .votelinks { min-width: 32px; }
    .votearrow { background: var(--accent-light); border-radius: var(--radius); color: var(--accent); display: block; width: 24px; height: 24px; font-size: 16px; position: relative; top: 2px; transition: background 0.1s, color 0.2s; }
    .votearrow:hover { background: var(--accent); color: white; }
    .votearrow:after { content: "⇧"; }
    body:has(form[action="login"]) { padding: 32px; }
  </style>`;
  document.head.insertAdjacentHTML("beforeend", style1);
  const style2 = `
  <style>
    .modern-downvoted {
      background: linear-gradient(135deg, #eef6ff, #f7fbff);
      padding: 12px 16px;
      border-left: 4px solid #007BFF;
      border-radius: 4px;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      font-size: 16px;
      color: #333;
    }
    .modern-downvoted a {
      color: #007BFF;
      text-decoration: none;
      font-weight: 500;
    }
    .modern-downvoted a:hover {
      text-decoration: underline;
    }
    pre.modern-code {
      position: relative;
      background: #f7f7f7;
      color: #333;
      padding: 16px;
      border: 1px solid #ddd;
      border-radius: 4px;
      overflow: auto;
      font-family: 'Fira Code', Consolas, "Courier New", monospace;
      font-size: 15px;
      line-height: 1.6;
      margin-bottom: 1em;
    }
    pre.modern-code code {
      font-family: 'Fira Code', Consolas, "Courier New", monospace;
      color: inherit;
    }
    .copy-button {
      position: absolute;
      top: 8px;
      right: 8px;
      background: transparent;
      border: none;
      cursor: pointer;
      padding: 4px;
      transition: color 0.2s;
      color: #666;
    }
    .copy-button:hover {
      color: #000;
    }
    .copy-button.svg-icon {
      width: 20px;
      height: 20px;
      display: block;
    }
  </style>`;
  document.head.insertAdjacentHTML("beforeend", style2);
  document.querySelectorAll('.commtext a').forEach(link => {
    link.setAttribute('target', '_blank');
    link.setAttribute('rel', 'noopener noreferrer');
  });
  document.querySelectorAll('.commtext:not(.c00)').forEach(e => {
    e.parentElement.classList.add('downvoted');
  });
  const quotes = document.evaluate("//p[starts-with(., '>')] | //span[starts-with(., '>')]", document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  for (let i = 0; i < quotes.snapshotLength; i++){
    let n = quotes.snapshotItem(i),
        t = Array.from(n.childNodes).find(x => x.nodeType === Node.TEXT_NODE);
    if (t){
      let p = document.createElement('p');
      p.className = 'quote';
      p.innerText = t.data.replace(">", "").trim();
      n.replaceChild(p, n.firstChild);
    } else {
      n.classList.add('quote');
      n.innerText = n.innerText.replace(">", "").trim();
    }
  }
  const copyIcon = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="svg-icon"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.34315 8.34315 2 10 2H19C20.6569 2 22 3.34315 22 5V14C22 15.6569 20.6569 17 19 17H17V19C17 20.6569 15.6569 22 14 22H5C3.34315 22 2 20.6569 2 19V10C2 8.34315 3.34315 7 5 7H7V5ZM9 7H14C15.6569 7 17 8.34315 17 10V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H10C9.44772 4 9 4.44772 9 5V7ZM5 9C4.44772 9 4 9.44772 4 10V19C4 19.5523 4.44772 20 5 20H14C14.5523 20 15 19.5523 15 19V10C15 9.44772 14.5523 9 14 9H5Z" fill="currentColor"></path></svg>`;
  const checkIcon = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="svg-icon"><path d="M20.285 5.715a1 1 0 00-1.414 0L9 15.586l-3.871-3.87a1 1 0 00-1.414 1.414l4.578 4.578a1 1 0 001.414 0l10.578-10.578a1 1 0 000-1.414z" fill="currentColor"/></svg>`;
  document.querySelectorAll('pre').forEach(block => {
    block.classList.add('modern-code');
    const btn = document.createElement('button');
    btn.className = 'copy-button svg-icon';
    btn.innerHTML = copyIcon;
    btn.addEventListener('click', () => {
      navigator.clipboard.writeText(block.innerText).then(() => {
        btn.classList.add('copied');
        btn.innerHTML = checkIcon;
        setTimeout(() => {
          btn.classList.remove('copied');
          btn.innerHTML = copyIcon;
        }, 2000);
      });
    });
    block.style.position = 'relative';
    block.appendChild(btn);
  });
  document.querySelectorAll('.comment.downvoted .commtext').forEach(e => {
    if (!e.querySelector('.modern-downvoted')) {
      const wrapper = document.createElement('div');
      wrapper.className = 'modern-downvoted';
      wrapper.innerHTML = e.innerHTML;
      e.innerHTML = '';
      e.appendChild(wrapper);
    }
  });
})();