Несколько дней назад, @vconst написал пост "Верни трекер, Хабр! (С)" с воззваниями по поводу изменения функциональности трекера в новой версии Хабра.

У меня возникла идея, что используя только расширение браузера возможно реализовать трекер с фактически любой функциональностью.

В качестве Proof-of-Concept, я попробовал самую простую идею, добавить старую версию трекера в правый сайдбар. Всё получилось даже проще, чем казалось.

Исходный код скрипта
(function () {
    function injectTracker() {

        const MAX_TRACKS = 10;

        // To add the tracker as a first block on the right sidebar, change "false" to "true"
        const insertAsFirstBlock = false;

        // set "true" if you want to tract articles only with unseen comments
        const onlyWithUnseenComments = false;

        // styling
        const trackerBlockCss = `<style scoped>
        .post-info__title a { color: #444; }
        a[href*="#comments"] { color: #82a3b1; }
        a[href*="#first_unread"] { color: #cf0000; }
        span.tracker { position: absolute; right: 0; }
        span.remove-tracker { position: absolute; right: 0;  display: none; }
        li:hover > .post-info__title span.remove-tracker { display: inline-block; color: #888; }
        .post-info__title:hover span.remove-tracker:hover { color: #444; }
        .tracker-refresh-icon { cursor: pointer; }
        .icon-anim { animation-name: rotate; animation-duration: 1s; animation-iteration-count: infinite; animation-timing-function: linear; animation-play-state: running; } 
        </style>`;

        const deleteSvg = '<svg class="delete-icon"  xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="16" width="16"><path d="m16.5 33.6 7.5-7.5 7.5 7.5 2.1-2.1-7.5-7.5 7.5-7.5-2.1-2.1-7.5 7.5-7.5-7.5-2.1 2.1 7.5 7.5-7.5 7.5ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q4.15 0 7.8 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm0-3q7.1 0 12.05-4.975Q41 31.05 41 24q0-7.1-4.95-12.05Q31.1 7 24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24 41Zm0-17Z"/></svg>';
        const refeshSvg = '<svg class="tracker-refresh-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="20" width="20" ><path d="M24 40q-6.65 0-11.325-4.675Q8 30.65 8 24q0-6.65 4.675-11.325Q17.35 8 24 8q4.25 0 7.45 1.725T37 14.45V8h3v12.7H27.3v-3h8.4q-1.9-3-4.85-4.85Q27.9 11 24 11q-5.45 0-9.225 3.775Q11 18.55 11 24q0 5.45 3.775 9.225Q18.55 37 24 37q4.15 0 7.6-2.375 3.45-2.375 4.8-6.275h3.1q-1.45 5.25-5.75 8.45Q29.45 40 24 40Z"/></svg>';

        const oldTrackerBlock = animateRefreshIcon();

        fetch('https://habr.com/ru/tracker/')
            .then(response => response.text())
            .then(response => processTracker(response));

        function processTracker(response) {
            const m = response.match(/<form[^>]+id="tracker_feed_form"[^>]*>(.*?)<\/form>/s);
            if (!m)
                return;

            const d = document.querySelector('.sidebar_right');
            if (!d)
                return;

            oldTrackerBlock?.parentNode.removeChild(oldTrackerBlock);

            const parseDiv = document.createElement("div");
            parseDiv.innerHTML = m[1];

            const newDiv = addNewElement(d, 'div', 'default-block default-block_sidebar block-tracker', trackerBlockCss, insertAsFirstBlock);

            const headerDiv = addNewElement(newDiv, 'div', 'default-block__header', '<h3 class="default-block__header-title"><a href="https://habr.com/ru/tracker/" target="_blank">Tracker</a> <span class="tracker-refresh-btn" >' + refeshSvg + '</span></h3> ');
            headerDiv.querySelector('.tracker-refresh-btn').addEventListener('click', injectTracker);

            const newContent = addNewElement(newDiv, 'div', 'default-block__content');
            const newUl = addNewElement(newContent, 'ul', 'content-list content-list_most-read');

            let currentTrack = 0;

            [...parseDiv.querySelectorAll('table.tracker-table tr')].filter(el => {
                if (currentTrack >= MAX_TRACKS)
                    return false;

                currentTrack += addTrack(el, newUl);
            });

            if (currentTrack === 0)
                addNewElement(newContent, 'div', '', onlyWithUnseenComments ? 'No articles with new comments' : '<a href="https://habr.com/ru/tracker/" target="_blank">No articles to track</a>');

            parseDiv.remove();
        }

        function addTrack(el, newUl) {
            let firstTd = el.querySelector('td:nth-child(1)');
            if (!firstTd)
                return 0;

            const trackerHtml = el.querySelector('td:nth-child(3)')?.innerHTML;

            if (onlyWithUnseenComments && !trackerHtml?.includes('+'))
                return 0;

            const newLi = addNewElement(newUl, 'li', 'content-list__item content-list__item_devided post-info');
            const newTopDiv = addNewElement(newLi, 'div', 'tracker-stats-info');

            addNewElement(newTopDiv, 'span', 'post_author', firstTd?.innerHTML.replaceAll('<a ', '<a target="_blank" '));
            addNewElement(newTopDiv, 'span', 'tracker', trackerHtml.replaceAll('<a ', '<a target="_blank" '));

            const newPost = addNewElement(newLi, 'div', 'post-info__title', el.querySelector('td:nth-child(2)')?.innerHTML.replaceAll('<a ', '<a target="_blank" ') + `<span class="remove-tracker" title="Remove from tracker">${deleteSvg}</span>`);
            newPost.querySelector('.remove-tracker').addEventListener('click', removeTracker);

            return 1;
        }

        function removeTracker(event) {

            const arctileEl = event.currentTarget.parentNode.querySelector('* > a');
            if (!arctileEl)
                return;

            const m = arctileEl.getAttribute('href')?.match(/\/([0-9]+)\//);
            if (!m)
                return;

            const postId = m[1];

            event.currentTarget.classList.add('icon-anim');

            fetch('https://habr.com/json/tracker/feed/remove/',
                {
                    method: 'POST',
                    body: `post[${postId}]=on`,
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
                }).then(() => injectTracker());
        }

        function addNewElement(parentElement, tageName, className, html, makeAsFirstChild) {
            const newEl = document.createElement(tageName);

            if (makeAsFirstChild)
                parentElement.insertBefore(newEl, parentElement.firstChild);
            else
                parentElement.appendChild(newEl);

            if (className)
                newEl.className = className;

            if (html)
                newEl.innerHTML = html;

            return newEl;
        }

        function animateRefreshIcon() {
            let oldTrackerBlock = document.querySelector('div.block-tracker');
            if (oldTrackerBlock)
                document.querySelector('.tracker-refresh-icon')?.classList.add('icon-anim');

            return oldTrackerBlock;
        }
    }

    injectTracker();
})();

Если вы используете Greasemonkey  или подобное расширение, то можете добавить этот скрипт для сайта хабра и на каждой странице у вас будет новая секция "tracker".

Я использую кастомизированный HabrSanitizer и если вы также его поклонник и хотели бы добавить функциональность трекера к нему, то надо загрузить исходники к себе на локальный компьютер, добавить его в качестве локального расширения (Load Unpacked) и вставить код скрипта перед строчкой (на сегодня это строка 397)

const isOnPersonalPage = onPersonalPage(window.location.href);

Можно протестировать функциональность и без установки расширения. На любой странице хабра нужно открыть DevTools и вставить код этого скрипта в Console. Но конечно это единовременное изменение страницы и при навигации со страницы, трэкер блок будет удалён и на новой странице необходимо будет повторить процедуру ещё раз.

Ещё раз - это скрипт работает только для старой версии интерфейса хабра.

Если всё сделано правильно то вы увидете под блоком "Читают сейчас" новый блок, "Tracker".

Что-то типа:

Следующий шаг будет попробовать что-то типа этого для новой версии.

Единственная проблема, это то что продётся читать не одну дополнительную страницу, а десять или более. Так что чтобы не генерировать избыточную нагрузку, может быть Хабру имеет смысл не бороться с пользователями, а прислушаться и мигрировать старый трекер на новый интерфейс. Это должно быть довольно просто.

Всем удачных выходных!

Комментарии (6)


  1. CaptainFlint
    13.11.2022 15:58
    +1

    Я не вникал глубоко в код, но похоже, что скрипт просто читает существующий трекер. Но если старая версия будет удалена, то наверняка и доступ к старому трекеру исчезнет. Добавить элемент на страницу несложно, а вот вытащить информацию для отображения в этом элементе куда проблематичнее, если Хабр её не будет предоставлять в готовом виде.


    1. hbn3 Автор
      13.11.2022 16:11
      +2

      Основная мысль статьи сформулированна в последнем предложении. Что было бы не плохо прислушаться к аксакалам хабра. Тем более, они же не просят чего то нового. А взять старую версию и добавить как ещё один возможный тип трекера в новый интерфейс.

      Это на порядок легче чем что-то новое писать.

      Поэтому я и назвал статью «пятничный трекер», подразумевая что это небольшой мод для очень немногих. Новый же трекер будет для всех.

      Вытащить информацию конечно сложнее, но тоже вполне по силам. Правда нагрузка на сам сайт будет сильно больше.


      1. CaptainFlint
        13.11.2022 16:46
        +6

        Это, конечно, было бы неплохо, но пока незаметно, чтобы их как-то беспокоило мнение пользователей. Сколько времени прошло с появления новой версии, всё это время их клюют на тему трекера, и до сих пор нет не только подвижек, но даже и банального признания наличия проблемы.


  1. Neikist
    13.11.2022 16:45
    +10

    Сижу на старой версии из за двух вещей, старого трекера и загрузки всех комментариев сразу со статьей.


    1. hbn3 Автор
      14.11.2022 21:08

      загрузки всех комментариев сразу со статьей.

      А в чём проблема? У меня довольно быстро комметы подгружаются

      старого трекера

      Посмотрел и в новой версии можно в качестве трекера использовать "закладки". Т.е. вместо/вместе с трекером добавляешь статью в закладки и примерно тоже самое как в обычном трекере.

      Немножко подтюнил скрипт и теперь он работает в новой версии для закладок.

      Т.е. будет выглядить точно такое же как и скрипт из статьи, но с содержимым из закладок.

      Код скрипта для новой версии
      (function () {
          function injectTracker() {
      
              const MAX_TRACKS = 30;
              const MAX_PAGES = 3;
      
              // To add the tracker as a first block on the right sidebar, change "false" to "true"
              const insertAsFirstBlock = false;
      
              const userName = getUserName();
              if(!userName)
                  return;
      
                  // styling
              const trackerBlockCss = `<style scoped>
              @keyframes rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } }
              svg.tm-article-comments-counter-link__icon { display: none; }
              .post-info__title a { color: #444; }
              a[href*="#comments"] { color: #82a3b1; }
              a[href*="#first_unread"] { color: #cf0000; }
              .tracker-stats-info { display: flex; }
              .tm-user-info__username, span.tracker * { font-weight: normal; }
              span.tracker { position: absolute; right: 0; display: flex; margin-left: auto; padding-right: 1em; }
              span.tracker-hide-or-remove { right: 1em; display: none; position: absolute; background: white; }
              li:hover .tm-article-title span.tracker-hide-or-remove { display: inline-block; color: #888; }
              .tm-article-title:hover span.remove-tracker:hover { color: #444; }
              .tracker-refresh-icon, .delete-icon, .hide-icon { cursor: pointer; }
              .icon-anim { animation-name: rotate; animation-duration: 1s; animation-iteration-count: infinite; animation-timing-function: linear; animation-play-state: running; } 
              </style>`;
      
              const deleteSvg = '<svg class="delete-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="16" width="16"><path d="m16.5 33.6 7.5-7.5 7.5 7.5 2.1-2.1-7.5-7.5 7.5-7.5-2.1-2.1-7.5 7.5-7.5-7.5-2.1 2.1 7.5 7.5-7.5 7.5ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q4.15 0 7.8 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm0-3q7.1 0 12.05-4.975Q41 31.05 41 24q0-7.1-4.95-12.05Q31.1 7 24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24 41Zm0-17Z"/></svg>';
              const refeshSvg = '<svg class="tracker-refresh-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="20" width="20" ><path d="M24 40q-6.65 0-11.325-4.675Q8 30.65 8 24q0-6.65 4.675-11.325Q17.35 8 24 8q4.25 0 7.45 1.725T37 14.45V8h3v12.7H27.3v-3h8.4q-1.9-3-4.85-4.85Q27.9 11 24 11q-5.45 0-9.225 3.775Q11 18.55 11 24q0 5.45 3.775 9.225Q18.55 37 24 37q4.15 0 7.6-2.375 3.45-2.375 4.8-6.275h3.1q-1.45 5.25-5.75 8.45Q29.45 40 24 40Z"/></svg>';
              const hideSvg = '<svg class="hide-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" style="vertical-align: middle;" height="16" width="16"><path d="m31.45 27.05-2.2-2.2q1.3-3.55-1.35-5.9-2.65-2.35-5.75-1.2l-2.2-2.2q.85-.55 1.9-.8 1.05-.25 2.15-.25 3.55 0 6.025 2.475Q32.5 19.45 32.5 23q0 1.1-.275 2.175-.275 1.075-.775 1.875Zm6.45 6.45-2-2q2.45-1.8 4.275-4.025Q42 25.25 42.85 23q-2.5-5.55-7.5-8.775Q30.35 11 24.5 11q-2.1 0-4.3.4-2.2.4-3.45.95L14.45 10q1.75-.8 4.475-1.4Q21.65 8 24.25 8q7.15 0 13.075 4.075Q43.25 16.15 46 23q-1.3 3.2-3.35 5.85-2.05 2.65-4.75 4.65Zm2.9 11.3-8.4-8.25q-1.75.7-3.95 1.075T24 38q-7.3 0-13.25-4.075T2 23q1-2.6 2.775-5.075T9.1 13.2L2.8 6.9l2.1-2.15L42.75 42.6ZM11.15 15.3q-1.85 1.35-3.575 3.55Q5.85 21.05 5.1 23q2.55 5.55 7.675 8.775Q17.9 35 24.4 35q1.65 0 3.25-.2t2.4-.6l-3.2-3.2q-.55.25-1.35.375T24 31.5q-3.5 0-6-2.45T15.5 23q0-.75.125-1.5T16 20.15Zm15.25 7.1Zm-5.8 2.9Z"/></svg>';
      
              const oldTrackerBlock = animateRefreshIcon();
      
              let hiddenItems = localStorage.getItem('hbr_tracker');
              hiddenItems = hiddenItems ? JSON.parse(hiddenItems) : [];
      
              let currentTrack = 0;
      
              fetch(`https://habr.com/ru/users/${userName}/favorites/posts/`)
                  .then(response => response.text())
                  .then(response => processTracker(response));
      
              function processTracker(response) {
                  const d = document.querySelector('.tm-sexy-sidebar');
                  if (!d)
                      return null;
      
                  oldTrackerBlock?.parentNode.removeChild(oldTrackerBlock);
      
                  const articles = parseArticles(response);
      
                  const newDiv = addNewElement(d, 'section', 'tm-block tm-block tm-block_spacing-bottom block-tracker', trackerBlockCss, insertAsFirstBlock);
      
                  const headerDiv = addNewElement(newDiv, 'header', 'tm-block__header tm-block__header', '<div class="tm-block__header-container"><h2 class="tm-block__title"><a href="https://habr.com/ru/tracker/" target="_blank">Tracker</a> <span class="tracker-refresh-btn" >' + refeshSvg + '</h2></div> ');
                  headerDiv.querySelector('.tracker-refresh-btn').addEventListener('click', injectTracker);
      
                  const newContent = addNewElement(newDiv, 'div', 'tm-block__body tm-block__body');
                  const newUl = addNewElement(newContent, 'ul', 'tm-article-list-block__list');
      
                  if (articles)
                      addArticles(articles, 1, newUl, newContent);
              }
      
              function addArticles(articles, pageId, newUl, newContent) {
                  articles.filter(el => {
                      if (currentTrack >= MAX_TRACKS)
                          return false;
      
                      currentTrack += addTrack(el, newUl);
                  });
      
                  if (currentTrack === 0)
                      addNewElement(newContent, 'p', '', 'No articles with new comments');
                  else if (currentTrack < MAX_TRACKS && pageId < MAX_PAGES && articles.length == 20)
                      processNextPage(pageId + 1, newUl, newContent);
              }
      
              function parseArticles(response) {
                  const m = response.match(/<div[^>]+data-test-id="articles-list"[^>]*>(.*?)<\/body>/s);
                  if (!m)
                      return null;
      
                  const parseDiv = document.createElement("div");
                  parseDiv.innerHTML = m[0];
      
                  return [...parseDiv.querySelectorAll('div.tm-articles-list article')];
              }
      
              function processNextPage(pageId, newUl, newContent) {
                  setTimeout(() => {
                      fetch(`https://habr.com/ru/users/${userName}/favorites/posts/page${pageId}/`)
                          .then(response => response.text())
                          .then(response => {
                              const articles = parseArticles(response);
                              if (articles)
                                  addArticles(articles, pageId, newUl, newContent);
                          });
                  }, 0);
              }
      
              function addTrack(el, newUl) {
                  const firstTd = el.querySelector('div.tm-article-snippet__meta span.tm-user-info__user');
                  if (!firstTd)
                      return 0;
      
                  const trackerHtml = el.querySelector('div.tm-article-comments-counter-link')?.innerHTML;
      
                  if (!trackerHtml?.includes('+'))
                      return 0;
      
                  const articleId = parseInt(el.getAttribute('id'));
      
                  if (hiddenItems.includes(articleId))
                      return 0;
      
                  const newLi = addNewElement(newUl, 'li', 'tm-article-list-block__item');
                  const newArcticle = addNewElement(newLi, 'article', 'tm-article-snippet-block-sidebar tm-article-snippet-block-sidebar_preview');
      
                  const newTopDiv = addNewElement(newArcticle, 'div', 'tracker-stats-info');
      
                  addNewElement(newTopDiv, 'span', 'post_author', firstTd?.innerHTML.replaceAll('<a ', '<a target="_blank" '));
                  addNewElement(newTopDiv, 'span', 'tracker', trackerHtml.replaceAll('<a ', '<a target="_blank" '));
      
                  const newPost = addNewElement(newArcticle, 'h2', 'tm-article-title tm-article-title_sidebar', `<span class="tracker-hide-or-remove"><span class="hide-tracker" title="Hide from tracker">${hideSvg}</span>&nbsp;<span class="remove-tracker" title="Remove from tracker">${deleteSvg}</span></span>` + el.querySelector('h2.tm-article-snippet__title')?.innerHTML.replaceAll('<a ', '<a target="_blank" ').replaceAll('"tm-article-snippet__title-link"', '"tm-article-title__link"'));
                  newPost.querySelector('.hide-tracker').addEventListener('click', hideTracker);
                  newPost.querySelector('.remove-tracker').addEventListener('click', removeTracker);
      
                  return 1;
              }
      
              function removeTracker(event) {
                  if (!confirm('Do you really want to delete it?'))
                      return;
      
                  const postId = getPostId(event.currentTarget);
                  if (!postId)
                      return;
      
                  event.currentTarget.querySelector('svg')?.classList.add('icon-anim');
      
                  fetch(`https://habr.com/kek/v1/articles/${postId}/bookmarks/remove`, {
                      method: 'POST',
                      body: {},
                      headers: { 'Content-Type': 'application/json' }
                  }).then(() => injectTracker());
              }
      
              function hideTracker(event) {
                  const postId = getPostId(event.currentTarget);
                  if (!postId)
                      return;
      
                  if (hiddenItems.length > 100)
                      hiddenItems.length = 99;
      
                  hiddenItems.unshift(postId);
      
                  localStorage.setItem('hbr_tracker', JSON.stringify(hiddenItems));
      
                  injectTracker();
              }
      
              function getPostId(el) {
                  const arctileEl = el.parentNode.parentNode.querySelector('* > a');
                  if (!arctileEl)
                      return;
      
                  const m = arctileEl.getAttribute('href')?.match(/\/([0-9]+)\//);
                  if (!m)
                      return null;
      
                  return parseInt(m[1]);
              }
      
              function addNewElement(parentElement, tageName, className, html, makeAsFirstChild) {
                  const newEl = document.createElement(tageName);
                  newEl.className = className ?? '';
                  newEl.innerHTML = html ?? '';
      
                  if (makeAsFirstChild)
                      parentElement.insertBefore(newEl, parentElement.firstChild);
                  else
                      parentElement.appendChild(newEl);
      
      
                  return newEl;
              }
      
              function animateRefreshIcon() {
                  const oldTrackerBlock = document.querySelector('section.block-tracker');
                  oldTrackerBlock?.querySelector('.tracker-refresh-icon')?.classList.add('icon-anim');
      
                  return oldTrackerBlock;
              }
      
              function getUserName()
              {
                  const userHref = document.querySelector('.tm-footer-menu__list-item > a[href*="/ru/users/"]')?.getAttribute("href");
                  if (!userHref)
                      return null;
      
                  const um = userHref.match(/\/ru\/users\/([^\/]+)\/posts\//)
                  if (!um)
                      return null;
      
                  return um[1];
              }
          }
      
          injectTracker();
      })();


      1. Neikist
        15.11.2022 10:30
        +1

        А в чём проблема? У меня довольно быстро комметы подгружаются

        Проблема в том что я нередко бываю в зонах с плохой связью. В которых комментарии ну никак не загружаются, а статей можно заранее наоткрывать почитать. Либо бывает начинаешь читать комментарии, первая порция подгрузилась, остальные нет. А непрочитанные сбрасываются, а именно возможность быстро по непрочитанным прыгать мне очень важна.