Greasy Fork is available in English.

MiaouDTC - DansTonChat.com

Improves browsing of quotes and comments on DansTonChat.com (also known as DTC). Miaou... Miaou... MiaouDTC

// ==UserScript==
// @name            MiaouDTC - DansTonChat.com
// @name:fr         MiaouDTC - DansTonChat.com
// @namespace       jlgrall_UserScripts
// @version         1.8
// @description     Improves browsing of quotes and comments on DansTonChat.com (also known as DTC). Miaou... Miaou... MiaouDTC
// @description:fr  Améliore la navigation des quotes et des commentaires sur DansTonChat.com (DTC pour les intimes). Miaou... Miaou... MiaouDTC
// @homepage        https://greasyfork.org/en/scripts/369900-miaoudtc-danstonchat-com
// @supportURL      https://greasyfork.org/en/scripts/369900-miaoudtc-danstonchat-com/feedback
// @contributionURL https://www.tipeee.com/danstonchat
// @author          jlgrall
// @license         MIT License
// @match           https://danstonchat.com/*
// @exclude         https://danstonchat.com/user/login*
// @exclude         https://danstonchat.com/myaccount*
// @grant           none
// ==/UserScript==

(function($, undefined) {
  'use strict';
  
  var $empty = $();
  
  var Miaou = window.Miaou = {
    // CSS rules:
    styleContent: [
      // GENERAL:
      'div.cls-cookie { display: none; }',
      '.contenu h1.miaouH1Search { cursor: pointer; }',
      '.item .item-content .decoration a.miaouPseudoLink { display: unset; max-width: unset; }',
      '.item .item-content .decoration a.miaouPseudoLink:hover { text-decoration: underline; }',
      
      // Pseudos:
      '.item .item-content .decoration.miaouPseudo-similarColor { margin-left: -4px; border-left: 4px solid red; }',
      
      // scoreRatio:
      'span.miaouScoreRatio { float: left; margin-left: -20px; }',
      
      // miaouComments Header:
      '.miaouComments-header { float: left; background: #ddd; }',
      '.miaouComments-header > span,',
      '.miaouComments-header > a { padding: 11px 10px 12px; font-size: 1.1em; _font-weight: bold; display: inline-block; color: #222; }',
      '.miaouComments-header > a:hover { background: #ccc; }',
      '@media (max-width: 530px) {',
      '  .miaouComments-header > a { text-indent: -9999px; line-height: 0; }', // Idea from: https://stackoverflow.com/questions/7896402/how-can-i-replace-text-with-css/22054588#22054588
      '  .miaouComments-header > a:after { content: "Comms"; text-indent: 0; line-height: initial; display: block; }',
      '}',
      '@media (max-width: 350px) {',
      '  .miaouComments-header > a { padding: 5px; }',
      '  .miaouComments-header > a:after { content: "C"; font-size: 1.4em; }',
      '}',
      
      // Toggle between buttons 'a.miaouComments-showComments' and 'a.miaouComments-hideComments':
      '.miaouItem:not(.miaouComments-hide) a.miaouComments-showComments,',
      '.miaouItem.miaouComments-hide a.miaouComments-hideComments { display: none; }',
      // Toggle text 'span.miaouComments-loadingComments':
      '.miaouItem.miaouComments-hide span.miaouComments-loadingComments { display: none; }',
      '.miaouItem:not(.miaouComments-hide):not(.miaouComments-loading) span.miaouComments-loadingComments { display: none; }',
      '.miaouItem.miaouComments-loading a.miaouComments-hideComments { display: none; }',
      
      // Highlight available Captain Obvious explanations:
      '.show_explanation { background: rgba(255, 255, 153, .6); }',
      '.miaouComments-body .explanation-text { background: rgba(255, 255, 153, .6); padding: 0 6px; }',
      '.miaouComments-body .explanation-text h2 { margin-top: 0; }',
      
      // miaouComments Wrapper and Body:
      '.miaouComments-wrapper { border-left: 10px solid #ddd; border-bottom: 10px solid #ddd; padding-left: 6px; }',
      '.miaouComments-body { max-height: 500px; overflow: auto; resize: vertical; padding-top: 6px; }',
      '.miaouComments-body:empty { display: none; }',
      '@media (max-height: 660px) { .miaouComments-body { max-height: 74vh; } }',
      '@media (max-height: 570px) { .miaouComments-body { max-height: 68vh; } }',
      '@media (max-height: 480px) { .miaouComments-body { max-height: 60vh; } }',
      '.miaouItem.miaouComments-hide .miaouComments-wrapper { display: none; }',
      
      // Linkified quotes:
      '.item-content a.miaou-linkifiedQuoteRef { display: unset; max-width: unset; color: #8cbd6a; }',
      '.item-content a.miaou-linkifiedQuoteRef:hover { color: #add095; }',
    ],
    
    // Comments loading states:
    COMMENTS_UNLOADED: 0,
    COMMENTS_LOADING: 1,
    COMMENTS_LOADED: 2,
    COMMENTS_ERROR: 3,
    getCommentsStateForItem: function($item) {
      var state = $item.data('data-miaou-commentsState');
      return state === undefined ? Miaou.COMMENTS_UNLOADED : state;
    },
    setCommentsStateForItem: function($item, state) {
      $item.data('data-miaou-commentsState', state);
    },
    
    rawPseudoToPseudo: {},
    pseudoToColor: {},
    pseudoToLink: {},
    
    itemsById: {},
    extractItemId: function($item) { // itemId is always a string (it should not be converted to a number)
      var item = $item[0];
      for (var i = 0; i < item.classList.length; i++) {
        var name = item.classList.item(i);
        if (name !== 'item') {
          var match = name.match(/^item(\d{1,9})$/);
          if (match) return match[1];
        }
      }
      return undefined;
    },
    initItem: function($item) {
      var itemId = Miaou.extractItemId($item);
      var itemURL = ($item.find('div.meta-infos span.comments > a').attr('href') || '').split('#')[0];
      var $score = $item.find('span.score');
      var scoreMatch = $score.text().match(/(\d{1,6})\s*\/\s*(\d{1,6})/);
      var scoreRatio = scoreMatch ? (scoreMatch[1] / scoreMatch[2]).toFixed(1) : NaN;
      var nbComments = parseInt($item.find('span.comments').text(), 10);
      
      Miaou.itemsById[itemId] = (Miaou.itemsById[itemId] || $empty).add($item);
      
      var $scoreRatio = $('<span class="miaouScoreRatio"> = ' + scoreRatio + '</span>');
      
      $item.data({
        'miaou-itemId': itemId,
        'miaou-itemURL': itemURL,
        'miaou-nbComments': nbComments,
        'miaou-$scoreRatio': $scoreRatio,
      });
      
      $item.addClass('miaouItem miaouItem' + itemId);
      
      Miaou._initItemPseudos($item);
      
      $scoreRatio.insertAfter($score);
      
      if (Miaou.pageHasItemsList) {
        Miaou._initItemComments($item);
      }
      else {
        $item.find('.item-content > a:not(.img)').replaceWith(function() {
          return this.childNodes;
        });
      }
      
      return $item;
    },
    _initItemPseudos: function($item) {
      var pseudoToItem$Els = {};
      $item.find('.item-content span.decoration').each(function(i, el) {
        var $el = $(el);
        var rawPseudo = $el.text();
        var pseudo = Miaou.rawPseudoToPseudo[rawPseudo];
        
        if (!(rawPseudo in Miaou.rawPseudoToPseudo)) {
          // Previous regex: replace(/^(?:\s|\*|\[[\d:h.]+\]|<[+@]?)+|(?:>|:|dit|\s)+$/g, '')
          pseudo = rawPseudo
            .replace(/^(?:\s|\*|\[[\d:h.]+\])+|(?::|dit|\s)+$/g, '')
            .replace(/^<[+@]?(.*)>$|^\[[+@]?(.*)\]$|^\((.*)\)$/g, '$1$2$3')
            .trim();
          if (pseudo === '') pseudo = undefined;
          Miaou.rawPseudoToPseudo[rawPseudo] = pseudo;
        }
        
        if (pseudo) {
          pseudoToItem$Els[pseudo] = (pseudoToItem$Els[pseudo] || $empty).add($el);
          
          if (!(pseudo in Miaou.pseudoToColor)) {
            var match = el.style.backgroundColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
            var color = match ? [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)] : undefined;
            Miaou.pseudoToColor[pseudo] = color;
          }
          
          var link = Miaou.pseudoToLink[pseudo];
          if (!link) {
            var url = '/search.html?search=' + encodeURIComponent(pseudo);
            link = Miaou.pseudoToLink[pseudo] = $('<a href="' + url + '" target="_blank" class="miaouPseudoLink"></a>');
          }
          $el.wrapInner(link.clone(true));
        }
      });
      
      // Compare pseudos colors and group them by similarity:
      var setOfSimilarSets = new Set();
      var pseudos = Object.keys(pseudoToItem$Els);
      for (var i = 0; i < pseudos.length; i++) {
        var p1 = pseudos[i];
        var c1 = Miaou.pseudoToColor[p1];
        for (var j = i + 1; j < pseudos.length; j++) {
          var p2 = pseudos[j];
          var c2 = Miaou.pseudoToColor[p2];
          var dist = Miaou.colorDistance(c1, c2);
          if (Miaou.debugColors) {
            var _c1 = pseudoToItem$Els[p1][0].style.backgroundColor;
            var _c2 = pseudoToItem$Els[p2][0].style.backgroundColor;
            console.log(
              "Color distance: %c" + dist.toFixed(2) + "%c '%c" + p1 + "%c' '%c" + p2 + "%c'",
              dist < Miaou.minColorDistance ? "color: red" : "", "",
              "color: black; font-weight: bold; background-color: " + _c1, "",
              "color: black; font-weight: bold; background-color: " + _c2, ""
            );
          }
          if (dist < Miaou.minColorDistance) {
            var foundSet = undefined;
            for (var similarSet of setOfSimilarSets) {
              if (similarSet.has(p1) || similarSet.has(p2)) {
                if (!foundSet) {
                  foundSet = similarSet;
                }
                else {  // Merge set with previously found set:
                  setOfSimilarSets.delete(similarSet);
                  similarSet.forEach(v => foundSet.add(v));
                }
                similarSet.add(p1).add(p2);
              }
            }
            if (!foundSet) setOfSimilarSets.add(new Set([p1, p2]));
          }
        }
      }
      
      // Add additional distinct colors to each set of similar pseudos:
      setOfSimilarSets.forEach(function(similarSet) {
        // For pseudos, DTC uses: hsv(X, 15, 95) converted to rgb.
        // We will use the CSS available: hsl(...) (Note that h is an angle so it wraps around)
        var bg = Miaou.pseudoToColor[similarSet.values().next().value];
        var h = Miaou.rgbToHsl(bg)[0];
        var step = 360 / (similarSet.size + 1);
        for (var pseudo of similarSet) {
          h += step;
          pseudoToItem$Els[pseudo].addClass("miaouPseudo-similarColor")
            .css("border-left-color", "hsl(" + Math.round(h) + ", 75%, 66%)");
        }
      });
      
      return $item;
    },
    _initItemComments: function($item) {
      var $commentsHeader = $('<div class="miaouComments-header"></div>');
      var $commentsWrapper = $('<div class="miaouComments-wrapper"></div>');
      var $commentsBody = $('<div class="miaouComments-body"></div>');
      
      var itemId = $item.data('miaou-itemId');
      var nbComments = $item.data('miaou-nbComments');
      var hrefShowComments = 'javascript:Miaou.showCommentsForItemId(\'' + itemId + '\')';
      var hrefHideComments = 'javascript:Miaou.hideCommentsForItemId(\'' + itemId + '\')';
      var $showComments = $('<a href="' + hrefShowComments + '" class="miaouComments-showComments">Afficher les commentaires (<span class="miaouComments-nbComments">' +  nbComments + '</span>)</a>');
      var $hideComments = $('<a href="' + hrefHideComments + '" class="miaouComments-hideComments">Masquer les commentaires (<span class="miaouComments-nbComments">' +  nbComments + '</span>)</a>');
      var $loadingComments = $('<span class="miaouComments-loadingComments">Chargement des commentaires...</span>');
      
      // Preload comments on mousedown:
      $showComments.one('mousedown', function() {
        Miaou.loadCommentsForItemId(itemId);
      });
      
      $item.data({
        'miaou-$commentsHeader': $commentsHeader,
        'miaou-$commentsWrapper': $commentsWrapper,
        'miaou-$commentsBody': $commentsBody,
      });
      
      $item.addClass('miaouComments-hide'); // TODO: rename/change to 'miaouComments-show' instead ?
      $commentsHeader.append($showComments, $hideComments, $loadingComments);
      $commentsHeader.insertBefore($item.find('.meta-bar .vote-plus'));
      $commentsWrapper.append($commentsBody).insertAfter($item.find('div.item-meta'));
      
      $item.find('.show_explanation').attr('href', hrefShowComments);
      
      Miaou.setCommentsStateForItem($item, Miaou.COMMENTS_UNLOADED);
      
      return $item;
    },
    loadCommentsForItemId: function(itemId) {
      var $items = Miaou.itemsById[itemId] || $empty;
      if ($items.length === 0) return console.error('Could not find element item with id: ' + itemId);
      
      var itemURL = undefined;
      $items.each(function(i, item) {
        var $item = $(item);
        if (Miaou.getCommentsStateForItem($item) === Miaou.COMMENTS_LOADING) return;
        
        $item.addClass('miaouComments-loading');
        Miaou.setCommentsStateForItem($item, Miaou.COMMENTS_LOADING);
        
        itemURL = $item.data('miaou-itemURL');
      });
      if (!itemURL) return; // Probably already loading.
      
      $.get(itemURL, 'html').then(function(data) {
        var $page = $($.parseHTML(data)).remove('script, style, link');
        var $comments = $page.find('.comment-list');
        var nbComments = $comments.find('.comment').length;
        var $explanationText = $page.find('.explanation-text').prependTo($comments);
        if (nbComments > 0) {
          $comments = Miaou.linkifyQuoteRefs($comments);
          return {$comments: $comments, state: Miaou.COMMENTS_LOADED, nbComments: nbComments};
        }
        else if ($page.find('#comments-bloc').length > 0) {
          $comments = $page.find('#comments-bloc').children();
          return {$comments: $comments, state: Miaou.COMMENTS_LOADED, nbComments: 0};
        }
        else {
          $comments = $.parseHTML('<div class="comment-list">Erreur: commentaires non trouvés.</div>');
          return $.Deferred().reject({$comments: $comments, state: Miaou.COMMENTS_ERROR});
        }
      }, function(jqXHR) {
        var $comments = $.parseHTML('<div class="comment-list">Erreur: status = "' + jqXHR.statusText + '"</div>');
        return {$comments: $comments, state: Miaou.COMMENTS_ERROR};
      }).always(function(data) {
        // In case content of page was modified during xhr:
        var $items = Miaou.itemsById[itemId] || $empty;
        if ($items.length === 0) return;
        
        $items.each(function(i, item) {
          var $item = $(item);
          
          if ('nbComments' in data) {
            if ($item.data('miaou-nbComments') !== data.nbComments) {
              $item.data('miaou-nbComments', data.nbComments);
              $item.find('.miaouComments-nbComments').text(data.nbComments);
            }
          }
          
          var $commentsBody = $item.data('miaou-$commentsBody');
          $commentsBody.empty().append(data.$comments);
          
          $item.removeClass('miaouComments-loading');
          Miaou.setCommentsStateForItem($item, data.state);
        });
      });
    },
    showCommentsForItemId: function(itemId) {
      var $items = Miaou.itemsById[itemId] || $empty;
      $items.each(function(i, item) {
        var $item = $(item);
        var state = Miaou.getCommentsStateForItem($item);
        if (state === Miaou.COMMENTS_UNLOADED) Miaou.loadCommentsForItemId(itemId);
        $item.removeClass('miaouComments-hide');
      });
    },
    hideCommentsForItemId: function(itemId) {
      var $items = Miaou.itemsById[itemId] || $empty;
      $items.each(function(i, item) {
        var $item = $(item);
        $item.addClass('miaouComments-hide');
      });
    },
    toggleCommentsForItemId: function(itemId) {
      var $items = Miaou.itemsById[itemId] || $empty;
      if ($items.filter(':not(.miaouComments-hide)').length > 0) {
        Miaou.hideCommentsForItemId(itemId);
      }
      else {
        Miaou.showCommentsForItemId(itemId);
      }
    },
    showCommentsForAllItems: function() {
      for (var itemId in Miaou.itemsById) Miaou.showCommentsForItemId(itemId);
    },
    hideCommentsForAllItems: function() {
      for (var itemId in Miaou.itemsById) Miaou.hideCommentsForItemId(itemId);
    },
    toggleCommentsForAllItems: function() {
      for (var itemId in Miaou.itemsById) Miaou.toggleCommentsForItemId(itemId);
    },
    linkifyQuoteRefs: function(node) {
      if (node.each) { // node is a jQuery object
        node.each(function(i, node) {
          Miaou.linkifyQuoteRefs(node);
        });
      }
      else if (node.hasChildNodes()) {
        for (var i = 0; i < node.childNodes.length; i++){
           Miaou.linkifyQuoteRefs(node.childNodes[i]);
        }
      }
      else if (node.nodeType === Node.TEXT_NODE
            && node.textContent.indexOf('#') > -1 // Quick test speeds up the process.
            && !node.parentNode.classList.contains('miaou-linkifiedQuoteRef')) {
        // Regex to find a DTC quote reference: a '#' followed by up to 9 digits,
        // and the reference must be separated from other words.
        var newHTML = node.textContent.replace(/\B#(\d{1,9})\b/g, '<a href="/$1.html" class="miaou-linkifiedQuoteRef">$&</a>');
        if (newHTML !== node.textContent) $(node).replaceWith(newHTML);
      }
      return node;
    },
    colorDistance: function(c1, c2) {
      // From: http://www.compuphase.com/cmetric.htm (via: https://stackoverflow.com/questions/5392061/algorithm-to-check-similarity-of-colors/40950076#40950076)
      var rmean = (c1[0] + c2[0]) / 2;
      var r = c1[0] - c2[0];
      var g = c1[1] - c2[1];
      var b = c1[2] - c2[2];
      return Math.sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8));
    },
    // From: https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion/9493060#9493060
    /**
     * Converts an RGB color value to HSL. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes r, g, and b are contained in the set [0, 255] and
     * returns h, s, and l in the set [0, 1].
     *
     * @param   {number}  r       The red color value
     * @param   {number}  g       The green color value
     * @param   {number}  b       The blue color value
     * @return  {Array}           The HSL representation
     */
    _rgbToHsl: function(r, g, b){
        r /= 255, g /= 255, b /= 255;
        var max = Math.max(r, g, b), min = Math.min(r, g, b);
        var h, s, l = (max + min) / 2;
    
        if(max == min){
            h = s = 0; // achromatic
        }else{
            var d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch(max){
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
        }
    
        return [h, s, l];
    },
    rgbToHsl: function(c) {
      var hsl = Miaou._rgbToHsl(c[0], c[1], c[2]);
      hsl[0] = Math.round(hsl[0] * 360);
      return hsl;
    },
  };
  
  
  
  
// MAIN:
  
  Miaou.pageHasItemsList = document.querySelector('div.items, div.item-listing, div.comment-listing') !== null;
  
  Miaou.$style = $('<style/>', {html: Miaou.styleContent.join('\n'), 'id': 'Miaou.$style'}).appendTo(document.head);
  
  if (window.location.pathname.startsWith('/search')) {
    $('.contenu h1').addClass('miaouH1Search').on('click', function() {
      $('.widget_search .icon-search').click();
    });
  }
  
  Miaou.minColorDistance = 30;
  //Miaou.debugColors = true;
  
  //Miaou.allowActionLoadAll = true;
  if (Miaou.pageHasItemsList && Miaou.allowActionLoadAll) {
    var $showAllComments = $('<a href="javascript:Miaou.showCommentsForAllItems()" class="miaouShowAllComments"><span>Afficher tous les commentaires</span></a>');
    $showAllComments.insertAfter('div.contenu a.submit.android');
    
    var $hideAllComments = $('<a href="javascript:Miaou.hideCommentsForAllItems()" class="miaouHideAllComments"><span>Masquer tous les commentaires</span></a>');
    $hideAllComments.insertAfter($showAllComments);
  }
  
  $('.item').each(function(i, item) {
  	Miaou.initItem($(item));
  });
  
  Miaou.linkifyQuoteRefs($('.item-content, .comment-content'));
  
  if (Miaou.pageHasItemsList) {
    // Remove original .show_explanation click events added in: https://danstonchat.com/themes/danstonchat2016/javascript/theme.js
    // Needs to be run after theme.js execution, so on document ready:
    $(function() {
      $('.show_explanation').off('click');
    });
  }
  
})(window.jQuery);