Advertisement
Guest User

8kun Bakertools v0.7.3

a guest
Sep 2nd, 2020
7,657
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*
  2. ==8Kun Baker Tools v0.7.3==
  3. We have entered the STORM. Be STRONG!
  4. GOD WINS!  Pray MEME LOVE!
  5.  
  6. For God and Country! WWG1WGA
  7.  
  8. ==Features:==
  9. '''Post Highlighting'''
  10. * Highlight posts that are marked notable (I.E. someone has replied and said
  11.   notable) in light green
  12. * Highlight nominating posts in dark green
  13. * Highlight nominating posts in posts mentions in light green
  14. * Highlight Q Posts in yellow
  15. * Highlight Q posts in mentions (I.E. on posts that get (YOU)'ed)
  16. * Highlight links to Q Posts in sparkle (Like q's trip)
  17. * Highlight previous bread links in blue
  18.  
  19. '''Navigation'''
  20. * Cycle through Q Posts
  21. * Cycle through (You)'s
  22. * Cycle through own posts
  23. * Jump To Bottom Link
  24. * Jump To Bottom Top link
  25. * NEW IN v0.7.0 Jump to last reading location (like when you post and it
  26.   sends you to bottom, you can jump right back)
  27. * Easy access to Breads via Bread List window
  28. * Scrollbar navigation shows location of Q/You/Notable/etc posts
  29.   * NEW IN v0.7.0: Hover over post marker to preview post
  30.   * Click on a post marker in scrollbar to jump to post
  31.  
  32. '''Filtering'''
  33. * Filter to only nominating and notable posts, Q posts, Q replies
  34. * Option to blur images until hover
  35. * Image blacklist (AKA the NOPE button)
  36. * NEW IN v0.7.0: SpamFader with multiple spam detection strategies:
  37.   * NameFag strategy: Marks namefags as spam
  38.   * Breadshitter strategy: Marks bread shitters as spam
  39.   * High post count strategy: Marks those with high post count as spam
  40.   * Flood fag strategy: Marks those who post in short intervals as spam
  41.   * Mark user as not spam button
  42.   * Spam badges tell WHY the algorithm marked as post as spam. TRANSPARENCY!
  43.  
  44. '''Customizable'''
  45. * NEW IN v0.7.0: Customizable post highlighting colors
  46. * Hide/Show features
  47. * Settings saved in localStorage
  48.  
  49. '''Notables'''
  50. * Generate notables post
  51. * Adds "Notable Nomination" button to posts that opens the
  52.   Quick Reply box and prefills it with a BAKER NOTABLE Template
  53.  
  54. '''Stats'''
  55.   * Thread stats overlay with
  56.     * color coded reply count that goes from green to red as bread ages
  57.     * UID Count
  58.   * Post rate chart shows how many posts per min
  59.  
  60. ==To Install:==
  61. 1. Copy this source code
  62. 2. Go to 8kun
  63. 3. Click "Options" in the top right
  64. 4. Choose "User JS" tab
  65. 5. Paste Baker tools JS
  66. 6. WWG1WGA
  67.  
  68. ==Changelog:==
  69. '''0.7.3'''
  70. * Fix previous bread vs next bread link highlighting logic
  71. * Update navigation controls colo(u)r to play nicer with dark theme
  72.  
  73. '''0.7.2'''
  74. * Use flex layout in bakerwindow controls
  75. * Reorder navcontrols
  76. * Reduce space of controls in boardlist
  77. * Disable spamfader new post event listener on disable
  78. * Don't mark q posts as notable
  79. * Make windows resizable width, fix table formatting in breadlist
  80. * Use boardlist height in current index calcs
  81.  
  82. '''0.7.1'''
  83. * Fix notable navigation in boardlist checkbox not working
  84. * Differentiate between previous and newer breads when highlighting
  85.  
  86. '''0.7.0'''
  87. * Switched color scheme to match other tools
  88. * Post Per Minute Graph
  89. * Spam Fading with multiple strategies and spam badges to tell why post is spam
  90. * Allow customization of post highligting colors
  91. * Add go back to last reading location button
  92. * Improve Q post detection (all past trip codes)
  93. * Add post preview on hover to Scrollbar Navigation
  94. * Navigation controls are now aware of current page location
  95. * Bugfixes
  96.  
  97. '''0.6.0'''
  98. * Navigation bar shows scroll location of q/you/notable
  99.   posts and allows jumping to posts
  100. * Notable navigation controls in baker window and board list
  101. * Persistent Image Blacklist (AKA Nope Button)
  102. * Many bugfixes
  103.  
  104. '''0.5.2'''
  105. * Fixes bread list table population bug
  106.  
  107. '''0.5.0'''
  108. * Option to show Q/(YOU)/Own Post navigation controls in the boardlist
  109. * Option to hide Notable nomination button
  110. * List of research breads
  111. * BakerTools settings are now saved in local storage
  112.  
  113. '''0.4.0'''
  114. * Option to blur images until hover
  115. * Adds a "Notable Nomination" button to posts that opens the Quick Reply
  116.   box and prefills it with a BAKER NOTABLE Template
  117. * Add Q Post navigation links to the Baker Window
  118. * Add (You) navigation links to the Baker Window
  119. * Add own post navigation links to the Baker Window
  120. * Cleaned up baker window design
  121.  
  122. * More code cleanup and linting changes
  123.  
  124. '''0.3.0'''
  125. * Highlights Q Posts with white BG -> DARK TO LIGHT!
  126. * Highlights Q posts in mentions (I.E. posts that get (YOU)'ed)
  127. * Highlights links to Q Posts
  128.  
  129. * Refactored code into classes for easier maint.
  130.  
  131. '''0.2.0'''
  132. * Highlight pb links
  133. * Thread stats overlay with
  134.     * color coded reply count that goes from green to red as bread ages
  135.     * UID Count
  136.     * Jump To Bottom Link
  137.     * Jump To Bottom Top link
  138.  
  139. '''0.1.0'''
  140. Initial release:
  141. * Highlight notables and nominators
  142. * Filter to only show notables and nominators
  143. * Create notables post
  144.  
  145. Version History:
  146. https://pastebin.com/L1p6iRzZ 0.7.2
  147. https://pastebin.com/dN5FhHCv 0.7.1
  148. https://pastebin.com/6XuDuHYu 0.7.0
  149. https://pastebin.com/YTSSmH7t 0.6.0
  150. https://pastebin.com/mPVxr7Lz 0.5.2
  151. https://pastebin.com/nEhm7yyY 0.5.1
  152. https://pastebin.com/i9sF0Rd3 0.4.0
  153. https://pastebin.com/kz9LrcE9 0.3.0
  154. https://pastebin.com/4aEFsPwK 0.2.0
  155. https://pastebin.com/eNmTtzdi 0.1.0
  156. */
  157. (function($) {
  158.     "use strict";
  159.    
  160.     /* globals $, board_name */
  161.     /* exported 8kun */
  162.    
  163.     /**
  164.      * Functions and vars related to EightKun functionality
  165.      */
  166.     class EightKun {
  167.       /**
  168.        * Get reply links in post
  169.        * @param {Element} post div.post
  170.        * @return {JQuery}
  171.        */
  172.       static getReplyLinksFromPost(post) {
  173.         return $(post).find(EightKun.REPLY_SELECTOR)
  174.             .filter(function(idx, link) {
  175.               return $(link).text().match(EightKun.REPLY_SHORTLINK_REGEX);
  176.             });
  177.       }
  178.    
  179.       /**
  180.        * Get the post number that is being replied to
  181.        * @param {Anchor} link
  182.        * @return {string}
  183.        */
  184.       static getPostNumberFromReplyLink(link) {
  185.         return $(link).text()
  186.             .match(EightKun.REPLY_SHORTLINK_REGEX)[1];
  187.       }
  188.    
  189.       /**
  190.        * Get time of post
  191.        * @param {Element} post div.post
  192.        * @return {number} post time in unixtime
  193.        */
  194.       static getPostTime(post) {
  195.         return $(post).find('.intro time').attr('unixtime');
  196.       }
  197.    
  198.       /**
  199.        * Get date of post
  200.        * @param {Element} post div.post
  201.        * @return {number} post time in unixtime
  202.        */
  203.       static getPostDateTime(post) {
  204.         return $(post).find('.intro time').attr('datetime');
  205.       }
  206.    
  207.       /**
  208.        * Get poster id of provided post
  209.        * @param {Element} post div.post
  210.        * @return {string} id of poster
  211.        */
  212.       static getPosterId(post) {
  213.         return $(post).find('p > span.poster_id').first().text();
  214.       }
  215.    
  216.       /**
  217.        * Get name from post
  218.        * @param {Element} post div.post
  219.        * @return {string} name of post
  220.        */
  221.       static getPostName(post) {
  222.         return $(post).find('.intro > label > .name').text();
  223.       }
  224.    
  225.       /**
  226.        * Get trip from post
  227.        * @param {Element} post div.post
  228.        * @return {string} trip of post
  229.        */
  230.       static getPostTrip(post) {
  231.         return $(post).find('.intro .trip').text();
  232.       }
  233.    
  234.       /**
  235.        * Get the opening post of the thread
  236.        * @return {Element} div.post
  237.        */
  238.       static getOpPost() {
  239.         return $(EightKun.OP_POST_SELECTOR);
  240.       }
  241.    
  242.       /**
  243.        * Get poster id of OP
  244.        * @return {number} poster id
  245.        */
  246.       static getOpPosterId() {
  247.         return EightKun.getPosterId(EightKun.getOpPost());
  248.       }
  249.    
  250.       /**
  251.        * Is the post made by op?
  252.        * @param {Element} post div.post
  253.        * @return {boolean} true if op's post
  254.        */
  255.       static isPostFromOp(post) {
  256.         return EightKun.getPosterId(post) === EightKun.getOpPosterId();
  257.       }
  258.    
  259.       /**
  260.        * Get the thread id
  261.        * @return {number} id of thread
  262.        */
  263.       static getThreadId() {
  264.         return $('.thread').get(0).id.split('_')[1];
  265.       }
  266.    
  267.       /**
  268.        * Use 8kun hide function on post
  269.        * @param {Element} post div.post
  270.        */
  271.       static hidePost(post) {
  272.         // TODO: implement it and use in spam and blacklist
  273.       }
  274.    
  275.       /**
  276.        * Get current board
  277.        * @return {string}
  278.        */
  279.       static getCurrentBoard() {
  280.         /* eslint-disable camelcase */
  281.         return board_name;
  282.       }
  283.    
  284.       /**
  285.        * Get post number of post
  286.        * @param {Element} post div.post
  287.        * @return {number} Number of the post
  288.        */
  289.       static getPostNumber(post) {
  290.         return post.id.split('_')[1];
  291.       }
  292.    
  293.       /**
  294.        * Get the top boardlist element
  295.        * @return {Element} div.boardlist
  296.        */
  297.       static getTopBoardlist() {
  298.         return $(EightKun.TOP_BOARDLIST_SELECTOR).get(0);
  299.       }
  300.     }
  301.     EightKun.POST_SELECTOR = 'div.post';
  302.     EightKun.POST_REPLY_SELECTOR = 'div.post.reply';
  303.     EightKun.OP_POST_SELECTOR = 'div.post.op';
  304.     EightKun.POST_BODY_SELECTOR = '.body';
  305.     EightKun.POST_MODIFIED_SELECTOR = '.post_modified';
  306.     EightKun.NEW_POST_EVENT = 'new_post';
  307.     EightKun.OP_SUBJECT_SELECTOR = '.post.op > p > label > span.subject';
  308.     EightKun.REPLY_SELECTOR = 'div.body:first a:not([rel="nofollow"])';
  309.     EightKun.REPLY_SHORTLINK_REGEX = /^>>(\d+)$/;
  310.     EightKun.REPLY_REGEX = /highlightReply\('(.+?)'/;
  311.     EightKun.BOARDLIST_SELECTOR = `.boardlist`;
  312.     EightKun.TOP_BOARDLIST_SELECTOR = `${EightKun.BOARDLIST_SELECTOR}:first`;
  313.    
  314.     /**
  315.      * Wrapper for 8kun active_page variable to determine the type of
  316.      * page the user is on.
  317.      */
  318.     class ActivePage {
  319.       /**
  320.        * Are we currently on the thread index page?
  321.        * @return {boolean} True if on index
  322.        */
  323.       static isIndex() {
  324.         return window.active_page == ActivePage.Index;
  325.       }
  326.    
  327.       /**
  328.        * Are we currently on the thread catalog page?
  329.        * @return {boolean} True if on catalog
  330.        */
  331.       static isCatalog() {
  332.         return window.active_page == ActivePage.Catalog;
  333.       }
  334.    
  335.       /**
  336.        * Are we on a thread page?
  337.        * @return {boolean} True if on thread
  338.        */
  339.       static isThread() {
  340.         return window.active_page == ActivePage.Thread;
  341.       }
  342.     }
  343.     ActivePage.Index = 'index';
  344.     ActivePage.Catalog = 'catalog';
  345.     ActivePage.Thread = 'thread';
  346.    
  347.    
  348.     /* globals $ */
  349.     /* exported ColorPicker */
  350.    
  351.     /**
  352.      * A color picker control that saves to localStorage
  353.      */
  354.     class ColorPicker {
  355.       /**
  356.        * Construct color picker
  357.        *
  358.        * @param {string} label The label for the control
  359.        * @param {string} title Mouseover title
  360.        * @param {string} setting localStorage setting name
  361.        * @param {string} defaultValue the default color when setting is missing
  362.        * @param {Function} changeHandler handler for value changes. Passes color
  363.        */
  364.       constructor(label, title, setting, defaultValue, changeHandler) {
  365.         this.styleId = 'bakertools-colorpickers-styles';
  366.         this.class = 'bakertools-colorpicker';
  367.         this.labelClass = 'bakertools-colorpicker-label';
  368.         this.inputClass = 'bakertools-colorpicker-input';
  369.         this.resetButtonClass = 'bakertools-colorpicker-reset';
  370.         this.changeHandler = changeHandler;
  371.    
  372.         this.label = label;
  373.         this.title = title;
  374.         this.setting = setting;
  375.         this.defaultValue = defaultValue;
  376.         this.strippedName = label.replace(/(\s|\(|\)|'|"|:)/g, '');
  377.         this.defaultValue = defaultValue;
  378.    
  379.         this._createStyles();
  380.         this._createElement();
  381.       }
  382.    
  383.       /**
  384.        * Create the HTML Element
  385.        */
  386.       _createElement() {
  387.         this.element = $(`
  388.         <div class='${this.class}'>
  389.           <label class='${this.labelClass}'
  390.             for="${this.strippedName}" title="${this.title}" >
  391.             ${this.label}:
  392.           </label>
  393.         </div>
  394.         `).get(0);
  395.    
  396.         this.input = $(`
  397.         <input type="color" class='${this.inputClass}'
  398.             id="${this.strippedName}" title="${this.title}" />
  399.         `).get(0);
  400.         $(this.element).append(this.input);
  401.    
  402.         $(this.input).change(function(e) {
  403.           this.setColor(this.input.value);
  404.         }.bind(this));
  405.    
  406.         this.reset = $(`
  407.         <button class='${this.resetButtonClass}' title="Reset to Default">
  408.           <i class="fa fa-undo"></i>
  409.         </button>
  410.         `).get(0);
  411.         $(this.element).append(this.reset);
  412.    
  413.         $(this.reset).click(function(e) {
  414.           e.preventDefault();
  415.           this.setColor(this.defaultValue);
  416.         }.bind(this));
  417.    
  418.         this.setColor(localStorage.getItem(this.setting) || this.defaultValue);
  419.       }
  420.    
  421.       /**
  422.        * Set the color
  423.        * @param {string} color valid css color string
  424.        */
  425.       setColor(color) {
  426.         localStorage.setItem(this.setting, color);
  427.         this.input.value = color;
  428.         this.changeHandler(color);
  429.       }
  430.    
  431.       /**
  432.        * Get the color
  433.        * @return {string} color
  434.        */
  435.       getColor() {
  436.         return this.input.value;
  437.       }
  438.       /**
  439.        * Create styles for the control
  440.        */
  441.       _createStyles() {
  442.         if ($(`#${this.styleId}`).length) {
  443.           return;
  444.         }
  445.         $('head').append(`
  446.         <style id='${this.styleId}'>
  447.           .${this.class} {
  448.             display: flex;
  449.             align-items: center;
  450.           }
  451.          
  452.           .${this.class} .${this.labelClass} {
  453.             flex-grow: 1;
  454.           }
  455.          
  456.           .${this.class} .${this.inputClass} {
  457.             margin-right: .5em;
  458.           }
  459.    
  460.           .${this.resetButtonClass} {
  461.             padding: 0;
  462.             background-color: Transparent;
  463.             background-repeat: no-repeat;
  464.             border: none;
  465.             cursor: pointer;
  466.             overflow: hidden;
  467.             outline: none;
  468.           }
  469.         </style>
  470.         `);
  471.       }
  472.     }
  473.    
  474.     /* global $, debounce, EightKun */
  475.     /**
  476.      * Creates first, prev, next, last navigation controls
  477.      */
  478.     class NavigationControl {
  479.       /**
  480.        * Construct navigatio control manager object
  481.        *
  482.        * @param {string} label the label for the control
  483.        * @param {Function} updateFunction Called to get latest data
  484.        * @param {string} updateEventName Called to get latest data
  485.        */
  486.       constructor(label, updateFunction, updateEventName) {
  487.         const strippedName = label.replace(/(\s|\(|\)|'|"|:)/g, '');
  488.         this.styleId = 'bakertools-navigationcontrol-styles';
  489.         this.label = label;
  490.         this.updateFunction = updateFunction;
  491.         this.updateEventName = updateEventName;
  492.         this.list = this.updateFunction();
  493.    
  494.         this.currentIndex = -1;
  495.         const instanceId = $(NavigationControl.containerClass).length;
  496.         this.navigationClass = `bakertools-navcontrol-${strippedName}`;
  497.         this.indexChangeEvent =
  498.           `bakertools-navcontrol-${strippedName}-index-changed`;
  499.    
  500.         this.currentIndexId = `${this.navigationClass}-current-index-${instanceId}`;
  501.         this.currentIndexClass = `bakertools-navcontrol-current-index`;
  502.         this.totalClass = `${this.navigationClass}-total`;
  503.         this.goToFirstClass = `${this.navigationClass}-goto-first`;
  504.         this.goToPreviousClass = `${this.navigationClass}-goto-prev`;
  505.         this.goToNextClass = `${this.navigationClass}-goto-next`;
  506.         this.goToLastClass = `${this.navigationClass}-goto-last`;
  507.    
  508.    
  509.         this._setupStyles();
  510.         this._createElement();
  511.         this.updateIndexFromCurrentScrollPosition();
  512.         this.updateIndexFromCurrentScrollPosition =
  513.             debounce(this.updateIndexFromCurrentScrollPosition, 500);
  514.         this._setupListeners();
  515.       }
  516.    
  517.       // TODO: switch to flexbox layout
  518.       /**
  519.        * setup styles for nav control
  520.        */
  521.       _setupStyles() {
  522.         if ($(`#${this.styleId}`).length) {
  523.           return;
  524.         }
  525.         const boardListNavSelector =
  526.           `${EightKun.BOARDLIST_SELECTOR} .${NavigationControl.containerClass}`;
  527.         $('head').append(`
  528.         <style id='${this.styleId}'>
  529.         ${boardListNavSelector}:before {
  530.           content: '[';
  531.           color: #89A;
  532.         }
  533.    
  534.         ${boardListNavSelector}:after {
  535.           content: ']';
  536.           color: #89A;
  537.         }
  538.        
  539.         ${boardListNavSelector} {
  540.           color: rgb(20, 137, 183);
  541.         }
  542.         </style>
  543.         `);
  544.       }
  545.    
  546.       /**
  547.        * Create nav element
  548.        */
  549.       _createElement() {
  550.         this.element = $(`
  551.         <span title="Navigate ${this.label}"
  552.             class="${NavigationControl.containerClass}">
  553.    
  554.             <label for="${this.navigationClass}">${this.label}:</label>
  555.             <span class="${this.navigationClass}
  556.                    ${NavigationControl.navigationControlClass}">
  557.                     <i class="fa fa-angle-double-left ${this.goToFirstClass}"></i>
  558.                     <i class="fa fa-angle-left ${this.goToPreviousClass}"></i>
  559.    
  560.                     <span class="${this.currentIndexClass}" id='${this.currentIndexId}'>
  561.               ${this.currentIndex+1}
  562.             </span>
  563.             :
  564.             <span class="${this.totalClass}">${this.list.length}</span>
  565.    
  566.             <i class="fa fa-angle-right ${this.goToNextClass}"></i>
  567.             <i class="fa fa-angle-double-right ${this.goToLastClass}"></i>
  568.           </span>
  569.         </span>
  570.         `).get(0);
  571.       }
  572.    
  573.       /**
  574.        * Setup button event listeners
  575.        */
  576.       _setupListeners() {
  577.         $(this.element).find('.'+this.goToFirstClass).click(function(e) {
  578.           this.goToFirstPost();
  579.         }.bind(this));
  580.    
  581.         $(this.element).find('.'+this.goToPreviousClass).click(function(e) {
  582.           this.goToPreviousPost();
  583.         }.bind(this));
  584.    
  585.         $(this.element).find('.'+this.goToNextClass).click(function(e) {
  586.           this.goToNextPost();
  587.         }.bind(this));
  588.    
  589.         $(this.element).find('.'+this.goToLastClass).click(function(e) {
  590.           this.goToLastPost();
  591.         }.bind(this));
  592.    
  593.         $(document).on(this.indexChangeEvent, function(e, index) {
  594.           if (this.currentIndex == index) return;
  595.           this._setCurrentIndex(index);
  596.         }.bind(this));
  597.    
  598.         $(document).on(this.updateEventName, function() {
  599.           this.list = this.updateFunction();
  600.           $(this.element).find(`.${this.totalClass}`).text(this.list.length);
  601.         }.bind(this));
  602.    
  603.         $(document).scroll(this.updateIndexFromCurrentScrollPosition.bind(this));
  604.       }
  605.    
  606.       /**
  607.        * Determine the current index based on scroll position
  608.        */
  609.       updateIndexFromCurrentScrollPosition() {
  610.         const boardListHeight = $(EightKun.getTopBoardlist()).height();
  611.         for (let i = 0; i < this.list.length; ++i) {
  612.           const post = this.list[i];
  613.           const boundingRect = post.getBoundingClientRect();
  614.    
  615.           const postTopAboveBottomOfScreen = boundingRect.top < window.innerHeight;
  616.           const postBottomBelowTopOfScreen = boundingRect.bottom > boardListHeight;
  617.           const currentPostIsInViewport = postTopAboveBottomOfScreen &&
  618.             postBottomBelowTopOfScreen;
  619.    
  620.           if (currentPostIsInViewport) {
  621.             this._setCurrentIndex(i);
  622.             break;
  623.           }
  624.    
  625.           const isFirstPost = i === 0;
  626.           const isBeforeFirstNotable = isFirstPost && !postTopAboveBottomOfScreen;
  627.           if (isBeforeFirstNotable) {
  628.             this._setCurrentIndex(-1);
  629.             break;
  630.           }
  631.    
  632.           const isLastPost = i === (this.list.length - 1);
  633.           const isPastLastNotable = isLastPost && !postBottomBelowTopOfScreen;
  634.           if (isPastLastNotable) {
  635.             this._setCurrentIndex(i + .5);
  636.             break;
  637.           }
  638.    
  639.           const nextPost = this.list[i+1];
  640.           const nextPostBounds = nextPost.getBoundingClientRect();
  641.           const nextPostIsBelowBottomOfScreen =
  642.             nextPostBounds.top >= window.innerHeight;
  643.           const inBetweenPosts = !postBottomBelowTopOfScreen &&
  644.             nextPostIsBelowBottomOfScreen;
  645.           if (inBetweenPosts) {
  646.             this._setCurrentIndex(i + .5);
  647.             break;
  648.           }
  649.         }
  650.       }
  651.    
  652.       /**
  653.        * Scroll to first post
  654.        */
  655.       goToFirstPost() {
  656.         if (!this.list.length) {
  657.           return;
  658.         }
  659.         this._setCurrentIndex(0);
  660.         this.scrollToCurrentPost();
  661.       }
  662.    
  663.       /**
  664.        * Scroll to next navigated post
  665.        */
  666.       goToPreviousPost() {
  667.         if (!this.list.length) {
  668.           return;
  669.         }
  670.         if (this.currentIndex <= 0) {
  671.           this._setCurrentIndex(this.list.length - 1);
  672.         } else {
  673.           this._setCurrentIndex(Math.ceil(this.currentIndex) - 1);
  674.         }
  675.         this.scrollToCurrentPost();
  676.       }
  677.       /**
  678.        * Scroll to next navigated post
  679.        */
  680.       goToNextPost() {
  681.         if (!this.list.length) {
  682.           return;
  683.         }
  684.         const lastPostIndex = this.list.length - 1;
  685.         if (this.currentIndex >= lastPostIndex) {
  686.           this._setCurrentIndex(0);
  687.         } else {
  688.           this._setCurrentIndex(Math.floor(this.currentIndex) + 1);
  689.         }
  690.         this.scrollToCurrentPost();
  691.       }
  692.    
  693.       /**
  694.        * Scroll the last  post in this bread into view
  695.        */
  696.       goToLastPost() {
  697.         if (!this.list.length) {
  698.           return;
  699.         }
  700.         const numPosts = this.list.length;
  701.         this._setCurrentIndex(numPosts - 1);
  702.         this.scrollToCurrentPost();
  703.       }
  704.    
  705.       /**
  706.        * Scrolls the current selected  post into view
  707.        */
  708.       scrollToCurrentPost() {
  709.         const post = this.list[this.currentIndex];
  710.         $(post).get(0).scrollIntoView();
  711.    
  712.         // Trigger events for other views of this data
  713.         $(document).trigger(this.indexChangeEvent,
  714.             this.currentIndex);
  715.    
  716.         const boardListHeight = $(EightKun.getTopBoardlist()).height();
  717.         window.scrollBy(0, -boardListHeight);
  718.       }
  719.    
  720.       /**
  721.        * Set internal index var and UI
  722.        * @param {number} index
  723.        */
  724.       _setCurrentIndex(index) {
  725.         this.currentIndex = index;
  726.         this._setCurrentIndexControlValue(index + 1);
  727.       }
  728.    
  729.       /**
  730.        * Sets the value of the current index in the UI
  731.        * @param {number} val
  732.        */
  733.       _setCurrentIndexControlValue(val) {
  734.         $('#'+this.currentIndexId).text(val);
  735.       }
  736.     }
  737.     NavigationControl.containerClass = `bakertools-navcontrol-container`;
  738.     NavigationControl.navigationControlClass = 'bakertools-navigation-control';
  739.    
  740.     /* global EightKun, $, NotableHighlighter */
  741.     /**
  742.     * Wrapper for a post nominated as notable
  743.     */
  744.     class NotablePost {
  745.       /**
  746.        * Construct an empty notable post object
  747.        */
  748.       constructor() {
  749.         this.element = null;
  750.         this.postNumber = null;
  751.         this.description = '[DESCRIPTION]';
  752.         this.nominatingPosts = [];
  753.       }
  754.    
  755.       /**
  756.        * Create a notable post from a nominating post
  757.        *
  758.        * @param {Element} nominatingPost A post that is nominating a notable
  759.        * @return {NotablePost} a Notable post or NullNotablePost if it fails
  760.        */
  761.       static fromNominatingPost(nominatingPost) {
  762.         const notables = [];
  763.         EightKun.getReplyLinksFromPost(nominatingPost)
  764.             .each(function(idx, link) {
  765.               const postNumber = EightKun.getPostNumberFromReplyLink(link);
  766.               const notablePostElement = $(`#reply_${postNumber}`).get(0);
  767.               if (window.bakerTools.qPostHighlighter.isQ(notablePostElement)) {
  768.                 return false;
  769.               }
  770.               if (!NotablePost.findNotableByPostNumber(postNumber)) {
  771.                 const notable = new NotablePost();
  772.    
  773.                 if (notablePostElement) {
  774.                   notable.setElement(notablePostElement);
  775.                 } else {
  776.                   // TODO: set pb description
  777.                   // get the json from the post number
  778.                   notable.postNumber = postNumber;
  779.                 }
  780.                 notable.addNominatingPost(nominatingPost);
  781.    
  782.                 NotablePost.addToListOfNotables(notable);
  783.                 notables.push(notable);
  784.    
  785.                 if (notable.element) { // Not pb will need to figure something out
  786.                   $(document).trigger(NotablePost.NEW_NOTABLE_POST_EVENT,
  787.                       notable.element);
  788.                 }
  789.               }
  790.             });
  791.         return notables;
  792.       }
  793.    
  794.       /**
  795.        * Add notable to list, and sort list
  796.        * @param {NotablePost} notable
  797.        */
  798.       static addToListOfNotables(notable) {
  799.         NotablePost._notables.push(notable);
  800.         NotablePost._notables.sort(function(n1, n2) {
  801.           if (n1.postNumber < n2.postNumber) {
  802.             return -1;
  803.           } else if ( n1.postNumber > n2.postNumber) {
  804.             return 1;
  805.           }
  806.           return 0;
  807.         });
  808.       }
  809.    
  810.       /**
  811.        * Is this a NullNotablePost
  812.        * @return {boolean} false
  813.        */
  814.       isNull() {
  815.         return false;
  816.       }
  817.    
  818.       /**
  819.        * @return {Array<NotablePost>} Array of the current notables
  820.        */
  821.       static getNotables() {
  822.         return NotablePost._notables;
  823.       }
  824.    
  825.       /**
  826.        * Get notable posts as regular 8kun div.post
  827.        * @return {Array} of div.post
  828.        */
  829.       static getNotablesAsPosts() {
  830.         return NotablePost._notables
  831.             .filter((n) => n.element !== null)
  832.             .map((n) => n.element);
  833.       }
  834.    
  835.       /**
  836.        * @arg {number} postNumber The post number of notable
  837.        * @return {NotablePost}
  838.        */
  839.       static findNotableByPostNumber(postNumber) {
  840.         return NotablePost._notables.find((notable) => notable.postNumber ==
  841.           postNumber);
  842.       }
  843.    
  844.       /**
  845.        * Set the element of the post
  846.        * @arg {Element} element
  847.        */
  848.       setElement(element) {
  849.         this.element = element;
  850.         this._markAsNotable(this.element);
  851.         this.description = element.querySelector('.body')
  852.             .innerText
  853.             .replace(/\n/g, ' ');
  854.         this.postNumber = $(this.element).find('.intro .post_no')
  855.             .text()
  856.             .replace('No.', '');
  857.       }
  858.    
  859.       /**
  860.        * Get the reply shortlink for the post
  861.        * @return {string}
  862.        */
  863.       shortLink() {
  864.         return '>>' + this.postNumber;
  865.       }
  866.    
  867.       /**
  868.        * Add a nominator to the notable
  869.        *
  870.        * @param {Element} nominatingPost A .div.post that nominates this post
  871.        */
  872.       addNominatingPost(nominatingPost) {
  873.         this.nominatingPosts.push(nominatingPost);
  874.         this._markAsNominator(nominatingPost);
  875.         this._markNominatorInMentions(nominatingPost);
  876.       }
  877.    
  878.       /**
  879.        * @arg {Element} nominatorPost .post
  880.        */
  881.       _markAsNominator(nominatorPost) {
  882.         nominatorPost.classList.add(NotableHighlighter.NOMINATOR_CLASS);
  883.       }
  884.    
  885.       /**
  886.        * @arg {Element} post .post
  887.        */
  888.       _markAsNotable(post) {
  889.         post.classList.add(NotableHighlighter.NOTABLE_CLASS);
  890.       }
  891.    
  892.    
  893.       /**
  894.        * Gives links to nominators a special style in notable mentions
  895.        *
  896.        * @param {Element} nominatingPost A .div.post that is nominating this
  897.        *  notable
  898.        */
  899.       _markNominatorInMentions(nominatingPost) {
  900.         if (!this.element) {
  901.           console.info(`Notable post is null - possible pb/lb`);
  902.           return;
  903.         }
  904.         const nominatingPostId = nominatingPost.id.replace('reply_', '');
  905.         $(this.element).find('.mentioned-'+nominatingPostId)
  906.             .addClass(NotableHighlighter.NOMINATOR_CLASS);
  907.       }
  908.     }
  909.     NotablePost._notables = [];
  910.     NotablePost.NULL = null; // NullNotablePost
  911.     NotablePost.NEW_NOTABLE_POST_EVENT = 'bakertools-new-notable-post-event';
  912.    
  913.     /* globals EightKun */
  914.     /**
  915.      * Research Bread Class
  916.      */
  917.     class ResearchBread {
  918.       /**
  919.        * Get an array of post bodies with dough posts filtered out
  920.        * @return {NodeList} of .post elements
  921.        */
  922.       static getPostsWithoutDough() {
  923.         const posts = Array.from(document
  924.             .querySelectorAll(EightKun.POST_SELECTOR));
  925.    
  926.         const filteredPosts = posts.filter(function(post) {
  927.           return !post.querySelector(EightKun.POST_BODY_SELECTOR)
  928.               .innerText.match(ResearchBread.DOUGH_POSTS_REGEX);
  929.         });
  930.    
  931.         return filteredPosts;
  932.       }
  933.    
  934.       /**
  935.        * Determine what the bread number is
  936.        * @return {number} the number of the research bread
  937.        */
  938.       static getBreadNumber() {
  939.         const breadNumberRegex = /#(.+?) /;
  940.         return document.querySelector(EightKun.OP_SUBJECT_SELECTOR)
  941.             .innerText
  942.             .match(breadNumberRegex)[1] || 'COULD NOT FIND BREAD NUMBER';
  943.       }
  944.    
  945.       /**
  946.        * Find the post with the dough
  947.        * @return {Element} div.post
  948.        */
  949.       static getDoughPost() {
  950.         const posts = Array.from(document
  951.             .querySelectorAll(EightKun.POST_SELECTOR));
  952.    
  953.         const dough = posts.find(function(post) {
  954.           return post.querySelector(EightKun.POST_BODY_SELECTOR)
  955.               .innerText.toUpperCase().match(ResearchBread.DOUGH_POST_TITLE);
  956.         });
  957.         return dough;
  958.       }
  959.     }
  960.     ResearchBread.BOARD_NAME = 'qresearch';
  961.     ResearchBread.WELCOME_POST_TITLE = 'Welcome To Q Research General';
  962.     ResearchBread.ANNOUNCEMENTS_POST_TITLE = 'Global Announcements';
  963.     ResearchBread.WAR_ROOM_POST_TITLE = 'War Room';
  964.     ResearchBread.ARCHIVES_POST_TITLE = 'QPosts Archives';
  965.     ResearchBread.DOUGH_POST_TITLE = 'DOUGH';
  966.     ResearchBread.DOUGH_POSTS_REGEX = new RegExp(
  967.         `^(${ResearchBread.WELCOME_POST_TITLE}|` +
  968.         `${ResearchBread.ANNOUNCEMENTS_POST_TITLE}|` +
  969.         `${ResearchBread.WAR_ROOM_POST_TITLE}|` +
  970.         `${ResearchBread.ARCHIVES_POST_TITLE}|` +
  971.         `${ResearchBread.DOUGH_POST_TITLE}).*`);
  972.    
  973.    
  974.     /* globals $, EightKun, debounce, POST_BACKGROUND_CHANGE_EVENT,
  975.        BakerWindow */
  976.     /* exported ScrollbarNavigation */
  977.     /**
  978.      * Scrollbar navigation
  979.      */
  980.     class ScrollbarNavigation {
  981.       /**
  982.        * Construct a scrollbar nav
  983.        * @param {Array} addPostEvents List of event names that produce posts
  984.        *        to show on scrollbar
  985.        */
  986.       constructor(addPostEvents = []) {
  987.         this.id = 'bakertools-scrollbar-navigation';
  988.         this.showScrollbarNavigationId = 'bakertools-show-scrollbar-nav';
  989.         this.width = '20px';
  990.         this.posts = [];
  991.         this.coordsToPost = new Map();
  992.         this.addPostEvents = addPostEvents;
  993.    
  994.         this.draw = debounce(this.draw, 80);
  995.    
  996.         this._setupBakerWindowControls();
  997.         this._setupStyles();
  998.         this._createElement();
  999.         this._readSettings();
  1000.         this._setupListeners();
  1001.       }
  1002.    
  1003.       /**
  1004.        * Read settings from localStorage
  1005.        */
  1006.       _readSettings() {
  1007.         let showScrollBar = JSON.parse(localStorage
  1008.             .getItem(ScrollbarNavigation.SHOW_SCROLLBAR_NAV));
  1009.    
  1010.         showScrollBar = showScrollBar === null ? true : showScrollBar;
  1011.    
  1012.         this.showScrollBar(showScrollBar);
  1013.       }
  1014.    
  1015.       /**
  1016.        * Add hide/show option to bakerwindow
  1017.        */
  1018.       _setupBakerWindowControls() {
  1019.         window.bakerTools.mainWindow
  1020.             .addNavigationOption(`
  1021.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  1022.           <label for="${this.showScrollbarNavigationId}"
  1023.             title="Show scrollbar navigation" >
  1024.             Show Scrollbar Navigation:
  1025.           </label>
  1026.           <input type="checkbox" id="${this.showScrollbarNavigationId}"
  1027.             title="Show scrollbar navigation" /><br />
  1028.         </div>
  1029.         `);
  1030.       }
  1031.    
  1032.       /**
  1033.        * Setup event listeners
  1034.        */
  1035.       _setupListeners() {
  1036.         $('#'+this.showScrollbarNavigationId).change(function(e) {
  1037.           this.showScrollBar(e.target.checked);
  1038.         }.bind(this));
  1039.    
  1040.         $(document).on(EightKun.NEW_POST_EVENT, this.draw.bind(this));
  1041.         $(window).on('resize', this.draw.bind(this));
  1042.         $(window).on(POST_BACKGROUND_CHANGE_EVENT, this.draw.bind(this));
  1043.    
  1044.         $('#'+this.id).click(function(e) {
  1045.           const post = this.findFirstPostUnderMouse(e.clientX, e.clientY);
  1046.           if (post) {
  1047.             $(post).get(0).scrollIntoView();
  1048.             window.scrollBy(0, -20);
  1049.           }
  1050.         }.bind(this));
  1051.    
  1052.         $(this.element).mousemove(function(e) {
  1053.           const [post, coords] = this.findFirstPostUnderMouse(e.clientX, e.clientY);
  1054.           const notOverAPost = !post;
  1055.           const hoveringOverADifferentPost = this.hoveringPost &&
  1056.               this.hoveringPost != post;
  1057.    
  1058.           if (notOverAPost || hoveringOverADifferentPost) {
  1059.             this.endHover();
  1060.           }
  1061.           if (this.hovering) {
  1062.             return;
  1063.           }
  1064.    
  1065.           const top = coords.top;
  1066.           if (post) {
  1067.             this.postHover(post, top);
  1068.           }
  1069.         }.bind(this));
  1070.    
  1071.         $(this.element).mouseout(this.endHover.bind(this));
  1072.    
  1073.         this.addPostEvents.forEach(function(eventName) {
  1074.           $(document).on(eventName, function(event, posts) {
  1075.             this.addPosts(posts);
  1076.           }.bind(this));
  1077.         }.bind(this));
  1078.       }
  1079.    
  1080.       /**
  1081.        * Find the first post that is under the mouse
  1082.        * @param {number} clientX x location of mouse
  1083.        * @param {number} clientY y location of mouse
  1084.        * @return {Array} div.post, [top,bottom] or null if not found
  1085.        */
  1086.       findFirstPostUnderMouse(clientX, clientY) {
  1087.         let post = null;
  1088.         let coords = null;
  1089.         for (const keyValue of this.coordsToPost) {
  1090.           coords = keyValue[0];
  1091.           // if (clientY >= (top - 10) && clientY <= (bottom+10)) {
  1092.           if (clientY >= (coords.top) && clientY <= (coords.bottom)) {
  1093.             post = keyValue[1];
  1094.             break;
  1095.           }
  1096.         }
  1097.         return [post, coords];
  1098.       }
  1099.    
  1100.       /**
  1101.        * Perform post hover functionality for provided post
  1102.        * @param {Element} post div.post
  1103.        * @param {number} hoverY y location to hover at
  1104.        */
  1105.       postHover(post, hoverY) {
  1106.         this.hovering = true;
  1107.         this.hoveringPost = post;
  1108.         const $post = $(post);
  1109.         if ($post.is(':visible') &&
  1110.             $post.offset().top >= $(window).scrollTop() &&
  1111.             $post.offset().top + $post.height() <=
  1112.               $(window).scrollTop() + $(window).height()) {
  1113.           // post is in view
  1114.           $post.addClass('highlighted');
  1115.         } else {
  1116.           const newPost = $post.clone();
  1117.           newPost.find('>.reply, >br').remove();
  1118.           newPost.find('a.post_anchor').remove();
  1119.    
  1120.           const postNumber = EightKun.getPostNumber(post);
  1121.           newPost.attr('id', 'post-hover-' + postNumber)
  1122.               .attr('data-board', EightKun.getCurrentBoard())
  1123.               .addClass('post-hover')
  1124.               .css('border-style', 'solid')
  1125.               .css('box-shadow', '1px 1px 1px #999')
  1126.               .css('display', 'block')
  1127.               .css('position', 'absolute')
  1128.               .css('font-style', 'normal')
  1129.               .css('z-index', '100')
  1130.               .css('left', '0')
  1131.               .css('margin-left', '')
  1132.               .addClass('reply')
  1133.               .addClass('post')
  1134.               .appendTo('.thread');
  1135.    
  1136.           // shrink expanded images
  1137.           newPost.find('div.file img.post-image').css({
  1138.             'display': '',
  1139.             'opacity': '',
  1140.           });
  1141.           newPost.find('div.file img.full-image').remove();
  1142.    
  1143.           let previewWidth = newPost.outerWidth(true);
  1144.           const widthDiff = previewWidth - newPost.width();
  1145.           const scrollNavLeft = $(this.element).offset().left;
  1146.           let left;
  1147.    
  1148.           if (scrollNavLeft < $(document).width() * 0.7) {
  1149.             left = scrollNavLeft + $(this.element).width();
  1150.             if (left + previewWidth > $(window).width()) {
  1151.               newPost.css('width', $(window).width() - left - widthDiff);
  1152.             }
  1153.           } else {
  1154.             if (previewWidth > scrollNavLeft) {
  1155.               newPost.css('width', scrollNavLeft - widthDiff);
  1156.               previewWidth = scrollNavLeft;
  1157.             }
  1158.             left = scrollNavLeft - previewWidth;
  1159.           }
  1160.           newPost.css('left', left);
  1161.    
  1162.           const scrollTop = $(window).scrollTop();
  1163.           let top = scrollTop + hoverY;
  1164.    
  1165.           if (top < scrollTop + 15) {
  1166.             top = scrollTop;
  1167.           } else if (top > scrollTop + $(window).height() - newPost.height() - 15) {
  1168.             top = scrollTop + $(window).height() - newPost.height() - 15;
  1169.           }
  1170.    
  1171.           if (newPost.height() > $(window).height()) {
  1172.             top = scrollTop;
  1173.           }
  1174.           newPost.css('top', top);
  1175.         }
  1176.       }
  1177.    
  1178.       /**
  1179.        * End hovering
  1180.        */
  1181.       endHover() {
  1182.         this.hovering = false;
  1183.         if (!this.hoveringPost) {
  1184.           return;
  1185.         }
  1186.    
  1187.         $(this.hoveringPost).removeClass('highlighted');
  1188.         if ($(this.hoveringPost).hasClass('hidden')) {
  1189.           $(this.hoveringPost).css('display', 'none');
  1190.         }
  1191.         $('.post-hover').remove();
  1192.       }
  1193.    
  1194.       /**
  1195.        * Show/hide scrollbar
  1196.        * @param {boolean} shouldShow Shows if true
  1197.        */
  1198.       showScrollBar(shouldShow) {
  1199.         $('#'+this.showScrollbarNavigationId).prop('checked',
  1200.             shouldShow);
  1201.    
  1202.         localStorage.setItem(ScrollbarNavigation.SHOW_SCROLLBAR_NAV, shouldShow);
  1203.    
  1204.         if (shouldShow) {
  1205.           $(`#${this.id}`).show();
  1206.         } else {
  1207.           $(`#${this.id}`).hide();
  1208.         }
  1209.       }
  1210.    
  1211.       /**
  1212.        * Setup styles for canvas
  1213.        */
  1214.       _setupStyles() {
  1215.         $('head').append(`
  1216.         <style id='${this.id + '-style'}'>
  1217.           #${this.id} {
  1218.             position: fixed;
  1219.             top: 0;
  1220.             right: 0;
  1221.             height: 100%;
  1222.             width: ${this.width};
  1223.             background: #000000;
  1224.             background: linear-gradient(
  1225.               90deg,
  1226.               rgba(0,0,0,1) 0%,
  1227.               rgba(92,92,92,1) 50%,
  1228.               rgba(0,0,0,1) 100%
  1229.             );
  1230.           }
  1231.         </style>
  1232.         `);
  1233.       }
  1234.    
  1235.       /**
  1236.        * Create the canvas
  1237.        */
  1238.       _createElement() {
  1239.         $(document.body).append(`
  1240.           <canvas id='${this.id}' width='${this.width}' height='300'>
  1241.           </canvas>
  1242.         `);
  1243.         this.element = $(`#${this.id}`).get(0);
  1244.       }
  1245.    
  1246.       /**
  1247.        * Draw the scrollbar
  1248.        */
  1249.       draw() {
  1250.         const canvas = this.element;
  1251.         canvas.height = window.innerHeight;
  1252.         const ctx = canvas.getContext('2d');
  1253.         if (!ctx) {
  1254.           console.info('no ctx - is the element created yet?');
  1255.           return;
  1256.         }
  1257.         ctx.clearRect(0, 0, canvas.width, canvas.height);
  1258.    
  1259.         const cachedHeight = $(document).height();
  1260.         const scrollHeight = canvas.height;
  1261.    
  1262.         this.coordsToPost = new Map();
  1263.    
  1264.         let lastCoords = null;
  1265.         this.posts.forEach(function(post, index) {
  1266.           const color = $(post).css('backgroundColor');
  1267.           const postRect = post.getBoundingClientRect();
  1268.           const scrollLocationPercentage =
  1269.               (window.scrollY + postRect.top) / cachedHeight;
  1270.           let drawLocation = scrollLocationPercentage * scrollHeight;
  1271.    
  1272.           const overlappingPrevious = lastCoords &&
  1273.               drawLocation <= (lastCoords.bottom + 2);
  1274.           if (overlappingPrevious) {
  1275.             drawLocation = lastCoords.bottom + 4;
  1276.           }
  1277.           const drawHeight = Math.max(
  1278.               (postRect.height / cachedHeight) * scrollHeight,
  1279.               5,
  1280.           );
  1281.           const coords = new ScrollbarCoordinates(drawLocation,
  1282.               drawLocation + drawHeight);
  1283.           this.coordsToPost.set(coords, post);
  1284.    
  1285.           ctx.fillStyle = color;
  1286.           ctx.fillRect(0, drawLocation, canvas.width, drawHeight);
  1287.           lastCoords = coords;
  1288.         }.bind(this));
  1289.       }
  1290.    
  1291.       /**
  1292.        * Add posts to scrollbar
  1293.        * @param {Element|Array} post div.post
  1294.        */
  1295.       addPosts(post) {
  1296.         if (Array.isArray(post)) {
  1297.           post.forEach((p) => this._addPost(p));
  1298.         } else {
  1299.           this._addPost(post);
  1300.         }
  1301.         this._sortPosts();
  1302.         this.draw();
  1303.       }
  1304.    
  1305.       /**
  1306.        * Add post to post array if not already included
  1307.        * @param {Element} post div.post
  1308.        */
  1309.       _addPost(post) {
  1310.         if (this.posts.includes(post)) {
  1311.           return;
  1312.         }
  1313.         this.posts.push(post);
  1314.       }
  1315.    
  1316.       /**
  1317.        * Sort posts by time
  1318.        */
  1319.       _sortPosts() {
  1320.         this.posts.sort(function(p1, p2) {
  1321.           const p1PostTime = EightKun.getPostTime(p1);
  1322.           const p2PostTime = EightKun.getPostTime(p2);
  1323.           if (p1PostTime < p2PostTime) {
  1324.             return -1;
  1325.           }
  1326.           if (p1PostTime > p2PostTime) {
  1327.             return 1;
  1328.           }
  1329.           return 0;
  1330.         });
  1331.       }
  1332.     }
  1333.     ScrollbarNavigation.SHOW_SCROLLBAR_NAV = 'bakertools-show-scrollbar-nav';
  1334.    
  1335.     /**
  1336.      * Coordinates on the scrollbar
  1337.      */
  1338.     class ScrollbarCoordinates {
  1339.       /**
  1340.        * Construct coords
  1341.        * @param {number} top top of rect
  1342.        * @param {number} bottom top of rect
  1343.        */
  1344.       constructor(top, bottom) {
  1345.         this.top = top;
  1346.         this.bottom = bottom;
  1347.       }
  1348.     }
  1349.    
  1350.     /* exported debounce, POST_BACKGROUND_CHANGE_EVENT */
  1351.     /**
  1352.     * Returns a function, that, as long as it continues to be invoked, will not
  1353.     * be triggered. The function will be called after it stops being called for
  1354.     * N milliseconds. If `immediate` is passed, trigger the function on the
  1355.     * leading edge, instead of the trailing.
  1356.     * https://davidwalsh.name/javascript-debounce-function
  1357.     *
  1358.     * @param {Function} func
  1359.     * @param {number} wait
  1360.     * @param {boolean} immediate
  1361.     * @return {Function} debounced function
  1362.     */
  1363.     function debounce(func, wait, immediate) {
  1364.       let timeout;
  1365.       return function(...args) {
  1366.         const context = this;
  1367.         const later = function() {
  1368.           timeout = null;
  1369.           if (!immediate) func.apply(context, args);
  1370.         };
  1371.         const callNow = immediate && !timeout;
  1372.         clearTimeout(timeout);
  1373.         timeout = setTimeout(later, wait);
  1374.         if (callNow) func.apply(context, args);
  1375.       };
  1376.     }
  1377.    
  1378.     const POST_BACKGROUND_CHANGE_EVENT = 'bakertools-post-background-change';
  1379.    
  1380.     /* globals $, EightKun */
  1381.     /* exported WindowElement */
  1382.     /**
  1383.      * Class for windows
  1384.      */
  1385.     class WindowElement {
  1386.       /**
  1387.        * Construct WindowElement
  1388.        * @param {string} windowName
  1389.        * @param {string} linkText
  1390.        */
  1391.       constructor(windowName, linkText) {
  1392.         this.styleId = 'bakertools-WindowElement-basestyles';
  1393.         this.id = `bakertools-${windowName}-window`;
  1394.         this.linkText = linkText;
  1395.         this.class = 'bakertools-WindowElement';
  1396.         this.headerClass = 'bakertools-WindowElement-header';
  1397.         this.windowCloseId = `bakertools-${windowName}-WindowElement-close`;
  1398.         this.windowCloseClass = `bakertools-WindowElement-close`;
  1399.         this.element = null;
  1400.    
  1401.         this._createWindowStyles();
  1402.         this._createElement();
  1403.         this._setupWindowLink();
  1404.       }
  1405.    
  1406.       /**
  1407.        * Create the window element
  1408.        */
  1409.       _createElement() {
  1410.         this.element = document.createElement('div');
  1411.         this.element.id = this.id;
  1412.         $(this.element).addClass(this.class);
  1413.    
  1414.         this.element.innerHTML = `
  1415.         <header class="${this.headerClass}">
  1416.           <h3>${this.linkText}</h3>
  1417.           <a id="${this.windowCloseId}" class='${this.windowCloseClass}'
  1418.             href="javascript:void(0)">
  1419.             <i class="fa fa-times"></i>
  1420.           </a>
  1421.         </header>
  1422.         `;
  1423.         document.body.appendChild(this.element);
  1424.    
  1425.         $(this.element).resizable({
  1426.           'handles': 'e, w',
  1427.         }).draggable();
  1428.         $(this.element).hide();
  1429.    
  1430.         $('#'+this.windowCloseId).click(function(e) {
  1431.           this.hide();
  1432.         }.bind(this));
  1433.       }
  1434.    
  1435.       /*
  1436.        * Create CSS styles needed by the window
  1437.        */
  1438.       _createWindowStyles() {
  1439.         if ($('#' + this.styleId).length) {
  1440.           return;
  1441.         }
  1442.         $('head').append(`
  1443.           <style id='${this.styleId}'>
  1444.           /*
  1445.            * ui-resizable styles: https://stackoverflow.com/a/11339280
  1446.            */
  1447.           .ui-resizable { position: relative;}
  1448.           .ui-resizable-handle {
  1449.             position: absolute;
  1450.             font-size: 0.1px;
  1451.             display: block;
  1452.           }
  1453.           .ui-resizable-disabled .ui-resizable-handle,
  1454.           .ui-resizable-autohide .ui-resizable-handle {
  1455.             display: none;
  1456.           }
  1457.           .ui-resizable-n {
  1458.             cursor: n-resize;
  1459.             height: 7px;
  1460.             width: 100%;
  1461.             top: -5px;
  1462.             left: 0;
  1463.           }
  1464.           .ui-resizable-s {
  1465.             cursor: s-resize;
  1466.             height: 7px;
  1467.             width: 100%;
  1468.             bottom: -5px;
  1469.             left: 0;
  1470.           }
  1471.           .ui-resizable-e {
  1472.             cursor: e-resize;
  1473.             width: 7px;
  1474.             right: -5px;
  1475.             top: 0;
  1476.             height: 100%;
  1477.           }
  1478.           .ui-resizable-w {
  1479.             cursor: w-resize;
  1480.             width: 7px;
  1481.             left: -5px;
  1482.             top: 0;
  1483.             height: 100%;
  1484.           }
  1485.           .ui-resizable-se {
  1486.             cursor: se-resize;
  1487.             width: 12px;
  1488.             height: 12px;
  1489.             right: 1px;
  1490.             bottom: 1px;
  1491.           }
  1492.           .ui-resizable-sw {
  1493.             cursor: sw-resize;
  1494.             width: 9px;
  1495.             height: 9px;
  1496.             left: -5px;
  1497.             bottom: -5px;
  1498.           }
  1499.           .ui-resizable-nw {
  1500.             cursor: nw-resize;
  1501.             width: 9px;
  1502.             height: 9px;
  1503.             left: -5px;
  1504.             top: -5px;
  1505.           }
  1506.           .ui-resizable-ne {
  1507.             cursor: ne-resize;
  1508.             width: 9px;
  1509.             height: 9px;
  1510.             right: -5px;
  1511.             top: -5px;
  1512.           }
  1513.    
  1514.           .${this.class} {
  1515.             width: 300px;
  1516.             background-color: rgb(214, 218, 240);
  1517.             position: fixed;
  1518.             z-index: 100;
  1519.             float: right;
  1520.             right:28.25px;
  1521.             border: 1px solid;
  1522.           }
  1523.    
  1524.           .${this.class} .${this.headerClass} {
  1525.             background: #98E;
  1526.             border: solid 1px;
  1527.             text-align: center;
  1528.             margin: 0px;
  1529.           }
  1530.          
  1531.           .${this.class} .${this.headerClass} h3 {
  1532.             margin: 0;
  1533.           }
  1534.    
  1535.           .${this.class} .${this.windowCloseClass} {
  1536.             top: 0px;
  1537.             right: 0px;
  1538.             position: absolute;
  1539.             margin-right: 3px;
  1540.             font-size: 20px;
  1541.           }
  1542.    
  1543.           .${this.class} details {
  1544.             padding: 5px;
  1545.           }
  1546.    
  1547.           .${this.class} summary {
  1548.             margin: 0 0 8px;
  1549.             font-weight: bold;
  1550.             border-bottom: solid 2px;
  1551.           }
  1552.         </style>
  1553.         `);
  1554.       }
  1555.    
  1556.       /**
  1557.        * Create link for show/hiding window, placed in boardlist bar
  1558.        */
  1559.       _setupWindowLink() {
  1560.         this.link = document.createElement('a');
  1561.         this.link.textContent = `[${this.linkText}]`;
  1562.         this.link.style.cssText = 'float: right;';
  1563.         this.link.title = this.linkText;
  1564.         this.link.href = 'javascript:void(0)';
  1565.         $(EightKun.getTopBoardlist()).append(this.link);
  1566.    
  1567.         this.link.onclick = this.toggle.bind(this);
  1568.       }
  1569.    
  1570.       /**
  1571.        * Setup timeout for updating bread list
  1572.        */
  1573.       _setupListeners() {
  1574.         // window.setTimeout(this.updateBreadList, 1000)
  1575.       }
  1576.    
  1577.       /**
  1578.        * Show the window
  1579.        */
  1580.       show() {
  1581.         $(this.element).css({'top': 15});
  1582.         $(this.element).show();
  1583.       }
  1584.    
  1585.       /**
  1586.        * Hide the window
  1587.        */
  1588.       hide() {
  1589.         $(this.element).hide();
  1590.       }
  1591.    
  1592.       /**
  1593.        * Is the window visible?
  1594.        * @return {boolean} true if window is visible
  1595.        */
  1596.       isVisible() {
  1597.         return $(this.element).is(':visible');
  1598.       }
  1599.    
  1600.       /**
  1601.        * Toggle visibility of window
  1602.        */
  1603.       toggle() {
  1604.         if (this.isVisible()) {
  1605.           this.hide();
  1606.         } else {
  1607.           this.show();
  1608.         }
  1609.       }
  1610.     }
  1611.    
  1612.     /* exported BakerWindow */
  1613.     /* global NavigationControl, $, WindowElement */
  1614.     /**
  1615.     * Baker Window
  1616.     */
  1617.     class BakerWindow extends WindowElement {
  1618.       /**
  1619.        * Construct Baker window element, register listeners
  1620.        */
  1621.       constructor() {
  1622.         super('baker', 'Baker Tools');
  1623.         this.bakerWindowStyleId = 'bakertools-bakerwindow-style';
  1624.         this.bakerWindowOptionsId = 'bakertools-window-options';
  1625.         this.bakerWindowColorOptionsId = 'bakertools-window-color-options';
  1626.         this.bakerWindowNavigationOptionsId =
  1627.             'bakertools-window-navigation-options';
  1628.         this.bakerWindowNotableOptionsId =
  1629.             'bakertools-window-notable-options';
  1630.         this.bakerWindowSpamOptionsId =
  1631.             'bakertools-window-spam-options';
  1632.         this.bakerWindowNavigationId = 'bakertools-window-navigation';
  1633.         this.bakerWindowBakerId = 'bakertools-window-baker';
  1634.         this.bakerWindowBodyId = 'bakertools-bakerwindow-body';
  1635.    
  1636.         this._createStyles();
  1637.         this._createBody();
  1638.       }
  1639.    
  1640.       /**
  1641.        * Create CSS styles needed by the window
  1642.        */
  1643.       _createStyles() {
  1644.         if ($('#' + this.bakerWindowStyleId).length) {
  1645.           return;
  1646.         }
  1647.         $('head').append(`
  1648.         <style id='${this.bakerWindowStyleId}'>
  1649.           #${this.id} #${this.bakerWindowNavigationId}
  1650.           .${NavigationControl.containerClass} {
  1651.             display: inline-block;
  1652.             width: 100%;
  1653.           }
  1654.    
  1655.           #${this.id} #${this.bakerWindowNavigationId}
  1656.           .${NavigationControl.navigationControlClass} {
  1657.             float: right;
  1658.           }
  1659.    
  1660.           ${BakerWindow.CONTROL_GROUP_SELECTOR} {
  1661.             display: flex;
  1662.           }
  1663.    
  1664.           ${BakerWindow.CONTROL_GROUP_SELECTOR} label {
  1665.             flex-grow: 1;
  1666.           }
  1667.         </style>
  1668.         `);
  1669.       }
  1670.    
  1671.       /**
  1672.        * Create the actual window HTML element
  1673.        */
  1674.       _createBody() {
  1675.         $('#'+this.id).append(`
  1676.         <form id="${this.bakerWindowBodyId}">
  1677.           <details id='${this.bakerWindowOptionsId}' open>
  1678.             <summary>Options</summary>
  1679.             <details id='${this.bakerWindowColorOptionsId}' open>
  1680.               <summary>Colors</summary>
  1681.             </details>
  1682.             <details id='${this.bakerWindowNavigationOptionsId}' open>
  1683.               <summary>Navigation</summary>
  1684.             </details>
  1685.             <details id='${this.bakerWindowNotableOptionsId}' open>
  1686.               <summary>Notables</summary>
  1687.             </details>
  1688.             <details id='${this.bakerWindowSpamOptionsId}' open>
  1689.               <summary>Spam</summary>
  1690.             </details>
  1691.    
  1692.    
  1693.           </details>
  1694.           <details id='${this.bakerWindowNavigationId}' open>
  1695.             <summary>Navigation</summary>
  1696.           </details>
  1697.           <details id='${this.bakerWindowBakerId}' open>
  1698.             <summary>Baker Tools</summary>
  1699.           </details>
  1700.         </form>
  1701.         `);
  1702.       }
  1703.    
  1704.       /**
  1705.        * Add form controls to options section of baker window
  1706.        * @arg {Element} htmlContentString form controls
  1707.        */
  1708.       addOption(htmlContentString) {
  1709.         $('#'+this.bakerWindowOptionsId).append(htmlContentString);
  1710.       }
  1711.    
  1712.       /**
  1713.        * Add form controls to notable options section of baker window
  1714.        * @arg {Element} htmlContentString form controls
  1715.        */
  1716.       addNotableOption(htmlContentString) {
  1717.         $('#'+this.bakerWindowNotableOptionsId)
  1718.             .append(htmlContentString);
  1719.       }
  1720.    
  1721.       /**
  1722.        * Add form controls to spam options section of baker window
  1723.        * @arg {Element} htmlContentString form controls
  1724.        */
  1725.       addSpamOption(htmlContentString) {
  1726.         $('#'+this.bakerWindowSpamOptionsId)
  1727.             .append(htmlContentString);
  1728.       }
  1729.    
  1730.       /**
  1731.        * Add form controls to navigation options section of baker window
  1732.        * @arg {Element} htmlContentString form controls
  1733.        */
  1734.       addNavigationOption(htmlContentString) {
  1735.         $('#'+this.bakerWindowNavigationOptionsId)
  1736.             .append(htmlContentString);
  1737.       }
  1738.    
  1739.       /**
  1740.        * Add form controls to color options section of baker window
  1741.        * @arg {Element} htmlContentString form controls
  1742.        */
  1743.       addColorOption(htmlContentString) {
  1744.         $('#'+this.bakerWindowColorOptionsId).append(htmlContentString);
  1745.       }
  1746.    
  1747.    
  1748.       /**
  1749.        * Add html elements to the navigation section of the baker window
  1750.        * @arg {Element} htmlContentString form controls
  1751.        */
  1752.       addNavigation(htmlContentString) {
  1753.         $('#'+this.bakerWindowNavigationId).append(htmlContentString);
  1754.       }
  1755.    
  1756.       /**
  1757.        * Add html elements to the baker section of the baker window
  1758.        * @arg {Element} htmlContentString form controls
  1759.        */
  1760.       addBaker(htmlContentString) {
  1761.         $('#'+this.bakerWindowBakerId).append(htmlContentString);
  1762.       }
  1763.     } // end class BakerWindow
  1764.     BakerWindow.CONTROL_GROUP_CLASS = 'bakertools-bakerwindow-control-group';
  1765.     BakerWindow.CONTROL_GROUP_SELECTOR =
  1766.       `.${BakerWindow.CONTROL_GROUP_CLASS}`;
  1767.    
  1768.     /* global $, BakerWindow */
  1769.     /**
  1770.     * Blur images until highlighted
  1771.     */
  1772.     class BlurImages {
  1773.       /**
  1774.        * Construct blur images object and setup styles
  1775.        */
  1776.       constructor() {
  1777.         this.blurImages = 'bakertools-blur-images';
  1778.         this.blurImagesStyleId = 'bakertools-blur-images-style';
  1779.         window.bakerTools.mainWindow.addOption(`
  1780.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  1781.           <label for="${this.blurImages}">Blur Images Until Hover</label>
  1782.           <input type="checkbox" id="${this.blurImages}"
  1783.             title="Blur images until mouse hover" /></br>
  1784.         </div>
  1785.         `);
  1786.    
  1787.         $('#'+this.blurImages).change(function(e) {
  1788.           this.setBlurImages(e.target.checked);
  1789.         }.bind(this));
  1790.    
  1791.         this._readSettings();
  1792.       }
  1793.    
  1794.       /**
  1795.        * Read settings from localStorage
  1796.        */
  1797.       _readSettings() {
  1798.         this.setBlurImages(JSON.parse(
  1799.             localStorage.getItem(
  1800.                 BlurImages.BLUR_IMAGES_SETTING),
  1801.         ));
  1802.       }
  1803.    
  1804.       /**
  1805.        * Set whether or not images are blurred
  1806.        * @param {boolean} blurImages if true, blur images
  1807.        */
  1808.       setBlurImages(blurImages) {
  1809.         $('#'+this.blurImages).prop('checked',
  1810.             blurImages);
  1811.    
  1812.         localStorage.setItem(BlurImages.BLUR_IMAGES_SETTING,
  1813.             blurImages);
  1814.    
  1815.         if (blurImages) {
  1816.           $(`<style id='${this.blurImagesStyleId}' type='text/css'>
  1817.               .post-image {
  1818.                   filter: blur(5px);
  1819.                   transition: all 233ms;
  1820.               }
  1821.               .post-image:hover {
  1822.                   filter: blur(.5px);
  1823.                   transition: all 89ms;
  1824.               }
  1825.           </style>`).appendTo('head');
  1826.         } else {
  1827.           $(`#${this.blurImagesStyleId}`).remove();
  1828.         }
  1829.       }
  1830.     }
  1831.     BlurImages.BLUR_IMAGES_SETTING = 'bakertools-blur-images';
  1832.    
  1833.    
  1834.    
  1835.     /* globals $, WindowElement, ResearchBread */
  1836.     /* exported BreadList */
  1837.     /**
  1838.      * Creates a list of breads for navigation comfyness
  1839.      */
  1840.     class BreadList extends WindowElement {
  1841.       /**
  1842.        * Construct breadlist object
  1843.        */
  1844.       constructor() {
  1845.         super('breadlist', 'Bread List');
  1846.         $('#'+this.id).css('height', '400px');
  1847.         this.breadListWindowHeaderId = 'bakertools-breadlist-window-header';
  1848.         this.breadListWindowCloseId = 'bakertools-breadlist-window-close';
  1849.         this.breadListWindowBody = 'bakertools-breadlist-window-body';
  1850.         this.breadListTable = 'bakertools-breadlist-table';
  1851.         this.lastUpdatedId = 'bakertools-breadlist-lastupdated';
  1852.    
  1853.         this._breads = [];
  1854.         ResearchBread.BOARD_NAME = 'qresearch';
  1855.         this.breadRegex = /(.+)\s+#(\d+):\s+(.+?$)/;
  1856.         // /\(.+\) #\(\d+\): \(.+?$\)/;
  1857.         this.indexPage = `${window.location.protocol}//${window.location.host}` +
  1858.             `/${ResearchBread.BOARD_NAME}/`;
  1859.    
  1860.         this._createBody();
  1861.         this._setupStyles();
  1862.         this.updateBreadList();
  1863.         this._setupListeners();
  1864.       }
  1865.    
  1866.       /**
  1867.        * setup table styles
  1868.        */
  1869.       _setupStyles() {
  1870.         // https://stackoverflow.com/questions/21168521/table-fixed-header-and-scrollable-body
  1871.         $('head').append(`
  1872.         <style id='baketools-breadlist-window-styles'>
  1873.           #${this.id} {
  1874.             right: 380px;
  1875.             width: 380px;
  1876.           }
  1877.    
  1878.           #${this.breadListWindowBody} {
  1879.             overflow-y: auto;
  1880.             height: 365px;
  1881.             font-size: .8em;
  1882.           }
  1883.    
  1884.           #${this.breadListTable} {
  1885.             border-collapse: collapse;
  1886.             border-spacing: 0px;
  1887.             margin: 0;
  1888.             width: 100%;
  1889.           }
  1890.    
  1891.           #${this.breadListTable} thead th {
  1892.             position: sticky;
  1893.             top: 0;
  1894.           }
  1895.    
  1896.           #${this.breadListTable} th,
  1897.           #${this.breadListTable} td {
  1898.             border: 1px solid #000;
  1899.             border-top: 0;
  1900.             padding: 1px 2px 1px 2px;
  1901.             margin: 0;
  1902.           }
  1903.           #${this.breadListTable} thead th {
  1904.             box-shadow: 1px 1px 0 #000;
  1905.           }
  1906.         </style>
  1907.         `);
  1908.       }
  1909.    
  1910.       /**
  1911.        * Create the actual window HTML element
  1912.        */
  1913.       _createBody() {
  1914.         $('#'+this.id).append(`
  1915.         <div id='${this.breadListWindowBody}'>
  1916.           <table id='${this.breadListTable}'>
  1917.             <thead>
  1918.               <tr>
  1919.                 <th>Group</th>
  1920.                 <th>No.</th>
  1921.                 <th>Bread</th>
  1922.                 <th>replies</th>
  1923.               </tr>
  1924.             </thead>
  1925.             <tbody>
  1926.             </tbody>
  1927.           </table>
  1928.         </div>
  1929.         <footer>
  1930.           Last Updated: <span id="${this.lastUpdatedId}"></span>
  1931.         </footer>
  1932.         `);
  1933.       }
  1934.    
  1935.       /**
  1936.        * Setup timeout for updating bread list
  1937.        */
  1938.       _setupListeners() {
  1939.         window.setInterval(function(e) {
  1940.           this.updateBreadList();
  1941.         }.bind(this), 1000 * 60 * 2.5); // 2.5min update
  1942.       }
  1943.    
  1944.       /**
  1945.        * Get the list of breads
  1946.        */
  1947.       updateBreadList() {
  1948.         this.breads = [];
  1949.    
  1950.         const promises = [];
  1951.         for (let page = 0; page < 3; page++) {
  1952.           promises.push(
  1953.               $.getJSON(this.indexPage + `${page}.json`,
  1954.                   this.parseIndex.bind(this)),
  1955.           );
  1956.         }
  1957.         Promise.all(promises).then(function() {
  1958.           this.breads.sort(function(a, b) {
  1959.             if (a.lastModified < b.lastModified) return -1;
  1960.             if (a.lastModified == b.lastModified) return 0;
  1961.             if (a.lastModified > b.lastModified) return 1;
  1962.           }).reverse();
  1963.           this.populateBreadTable();
  1964.         }.bind(this));
  1965.       }
  1966.    
  1967.       /**
  1968.        * Parse index json for breads
  1969.        * @param {Object} index
  1970.        */
  1971.       parseIndex(index) {
  1972.         if (index && index.threads) {
  1973.           index.threads.forEach(function(thread) {
  1974.             const op = thread.posts[0];
  1975.             const match = op.sub.match(this.breadRegex);
  1976.    
  1977.             if (match) {
  1978.               const researchGroup = match[1];
  1979.               const breadNumber = match[2];
  1980.               const breadName = match[3];
  1981.               this.breads.push(new Bread(
  1982.                   ResearchBread.BOARD_NAME,
  1983.                   researchGroup,
  1984.                   breadNumber,
  1985.                   breadName,
  1986.                   op.replies,
  1987.                   op.no,
  1988.                   op.last_modified,
  1989.               ));
  1990.             }
  1991.           }.bind(this)); // Index foreach
  1992.         } // if index and index.threads
  1993.       }
  1994.    
  1995.       /**
  1996.        * Populate the bread list table
  1997.        */
  1998.       populateBreadTable() {
  1999.         $(`#${this.breadListTable} tbody`).empty();
  2000.         this.breads.forEach(function(bread) {
  2001.           this._addBread(bread);
  2002.         }.bind(this));
  2003.    
  2004.         const lastUpdated = new Date();
  2005.         $('#'+this.lastUpdatedId).text(lastUpdated.toLocaleString());
  2006.       }
  2007.    
  2008.       /**
  2009.        * Add bread
  2010.        * @param {Bread} bread
  2011.        */
  2012.       _addBread(bread) {
  2013.         $(`#${this.breadListTable} tbody`).append(`
  2014.           <tr>
  2015.             <td><a href='${bread.url}'>${bread.researchGroup}</a></td>
  2016.             <td><a href='${bread.url}'>${bread.researchNumber}</a></td>
  2017.             <td><a href='${bread.url}'>${bread.breadName}</a></td>
  2018.             <td><a href='${bread.url}'>${bread.replies}</a></td>
  2019.           </tr>
  2020.         `);
  2021.       }
  2022.     }
  2023.    
  2024.     /**
  2025.      * Represents a research bread
  2026.      */
  2027.     class Bread {
  2028.       /**
  2029.        * Construct a bread
  2030.        *
  2031.        * @param {string} boardName
  2032.        * @param {string} researchGroup
  2033.        * @param {number} researchNumber
  2034.        * @param {string} breadName
  2035.        * @param {number} replies
  2036.        * @param {number} postId
  2037.        * @param {number} lastModified
  2038.        */
  2039.       constructor(boardName, researchGroup, researchNumber, breadName,
  2040.           replies, postId, lastModified) {
  2041.         this.boardName = boardName;
  2042.         this.researchGroup = researchGroup;
  2043.         this.researchNumber = researchNumber;
  2044.         this.breadName = breadName;
  2045.         this.replies = replies;
  2046.         this.postId = postId;
  2047.         this.lastModified = lastModified;
  2048.       }
  2049.    
  2050.       /**
  2051.        * Get bread url
  2052.        *
  2053.        * @return {string} url to bread
  2054.        */
  2055.       get url() {
  2056.         return `${window.location.protocol}//${window.location.host}` +
  2057.             `/${this.boardName}/res/${this.postId}.html`;
  2058.       }
  2059.     }
  2060.    
  2061.     /* global $, EightKun, BakerWindow */
  2062.     /**
  2063.     * Persistent image blacklist (AKA NOPE BUTTON)
  2064.     */
  2065.     class ImageBlacklist {
  2066.       /**
  2067.        * Construct ImageBlacklist object
  2068.        */
  2069.       constructor() {
  2070.         this.blacklist = [];
  2071.         this.styleId = 'bakertools-blacklist-style';
  2072.         this.postBlacklistButtonClass = 'bakertools-blacklist-post';
  2073.         this.imgBlacklistButtonClass = 'bakertools-blacklist-image';
  2074.         this.hidePostBlacklistButtonCheckboxId =
  2075.             'bakertools-hide-post-blacklist-buttons';
  2076.    
  2077.         this._setupBakerWindowControls();
  2078.         this._readSettings();
  2079.         this._setupStyles();
  2080.         this._setupListeners();
  2081.         this.removeBlacklistedImages();
  2082.         this.addBlacklistButtons();
  2083.       }
  2084.    
  2085.       /**
  2086.        * Add options to baker window
  2087.        */
  2088.       _setupBakerWindowControls() {
  2089.         window.bakerTools.mainWindow.addSpamOption(`
  2090.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2091.           <label for="${this.hidePostBlacklistButtonCheckboxId}"
  2092.             title="Hide post 'Blacklist' buttons" >
  2093.             Hide "Blacklist" buttons
  2094.           </label>
  2095.           <input type="checkbox" id="${this.hidePostBlacklistButtonCheckboxId}"
  2096.             title="Hide post 'Blacklist' buttons" /><br />
  2097.           </div>
  2098.         `);
  2099.       }
  2100.    
  2101.    
  2102.       /**
  2103.        * Show or hide the post blacklist buttons
  2104.        *
  2105.        * @param {boolean} hide
  2106.        */
  2107.       hidePostBlacklistButton(hide) {
  2108.         $('#'+this.hidePostBlacklistButtonCheckboxId).prop('checked',
  2109.             hide);
  2110.    
  2111.         localStorage.setItem(ImageBlacklist.HIDE_POST_BLACKLIST_BUTTON_SETTING,
  2112.             hide);
  2113.    
  2114.         const styleId = 'baker-tools-post-blacklist-button-style';
  2115.         if (hide) {
  2116.           $('head').append(`
  2117.             <style id='${styleId}'>
  2118.               .${this.postBlacklistButtonClass} {
  2119.                 display: none;
  2120.               }
  2121.           `);
  2122.         } else {
  2123.           $(`#${styleId}`).remove();
  2124.         }
  2125.       }
  2126.    
  2127.       /**
  2128.        * Setup styles for blacklist buttons
  2129.        */
  2130.       _setupStyles() {
  2131.         $('head').append(`
  2132.           <style id='${this.styleId}'>
  2133.             .${this.imgBlacklistButtonClass} {
  2134.               padding: 0px;
  2135.               background-color: Transparent;
  2136.               background-repeat: no-repeat;
  2137.               border: none;
  2138.               overflow: hidden;
  2139.               outline: none;
  2140.               cursor: pointer;
  2141.             }
  2142.           </style>
  2143.         `);
  2144.       }
  2145.    
  2146.       /**
  2147.        * Read settings from localstorage
  2148.        */
  2149.       _readSettings() {
  2150.         this.loadBlacklist();
  2151.    
  2152.         const hideBlacklistButton = JSON.parse(
  2153.             localStorage.getItem(ImageBlacklist.HIDE_POST_BLACKLIST_BUTTON_SETTING),
  2154.         );
  2155.         this.hidePostBlacklistButton(hideBlacklistButton);
  2156.       }
  2157.    
  2158.       /**
  2159.        * Setup new post event listeners
  2160.        */
  2161.       _setupListeners() {
  2162.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  2163.           this.addBlacklistButtonToPost(post);
  2164.         }.bind(this));
  2165.    
  2166.         $('#'+this.hidePostBlacklistButtonCheckboxId).change(function(e) {
  2167.           this.hidePostBlacklistButton(e.target.checked);
  2168.         }.bind(this));
  2169.       }
  2170.    
  2171.       /**
  2172.        * Load blacklist from localStorage
  2173.        */
  2174.       loadBlacklist() {
  2175.         this.blacklist = JSON.parse(localStorage.imageBlacklist || '[]');
  2176.       }
  2177.    
  2178.       /**
  2179.        * Save blacklist to localStorage
  2180.        */
  2181.       saveBlacklist() {
  2182.         localStorage.imageBlacklist = JSON.stringify(this.blacklist);
  2183.       }
  2184.    
  2185.       /**
  2186.        * Add MD5 of an image to the blacklist
  2187.        * @param {string} md5 md5 hash of image
  2188.        */
  2189.       addToBlacklist(md5) {
  2190.         if (md5 && -1 === this.blacklist.indexOf(md5)) {
  2191.           this.blacklist.push(md5);
  2192.         }
  2193.       }
  2194.    
  2195.       /**
  2196.        * Blacklist images in post
  2197.        * @param {Element} post
  2198.        */
  2199.       blacklistPostImages(post) {
  2200.         $(post).find(ImageBlacklist.POST_IMG_SELECTOR).each(function(i, postImage) {
  2201.           const md5 = postImage.getAttribute('data-md5');
  2202.           this.addToBlacklist(md5);
  2203.           this.deletePostImage(postImage);
  2204.         }.bind(this));
  2205.       }
  2206.    
  2207.       /**
  2208.        * Remove blacklist images on page load
  2209.        * @return {number} number of images removed
  2210.        */
  2211.       removeBlacklistedImages() {
  2212.         let removed = 0;
  2213.         $(ImageBlacklist.POST_IMG_SELECTOR).each(function(i, postImage) {
  2214.           if (-1 !== this.blacklist.indexOf(postImage.getAttribute('data-md5'))) {
  2215.             this.deletePostImage(postImage);
  2216.             removed += 1;
  2217.           }
  2218.         }.bind(this));
  2219.         return removed;
  2220.       }
  2221.    
  2222.       /**
  2223.        * Add blacklist buttons to post images
  2224.        */
  2225.       addBlacklistButtons() {
  2226.         $('div.post').each(function(i, post) {
  2227.           this.addBlacklistButtonToPost(post);
  2228.         }.bind(this));
  2229.       }
  2230.    
  2231.       /**
  2232.        * Add blacklist buttons to post
  2233.        * @param {Element} post div.post
  2234.        */
  2235.       addBlacklistButtonToPost(post) {
  2236.         const postImageCount = $(post)
  2237.             .find(ImageBlacklist.POST_IMG_SELECTOR).length;
  2238.    
  2239.         if (postImageCount == 0) {
  2240.           return;
  2241.         }
  2242.    
  2243.         const postBlacklistButton = document.createElement('button');
  2244.         $(postBlacklistButton).addClass(this.postBlacklistButtonClass);
  2245.         $(postBlacklistButton).append(`
  2246.         <i class="fa fa-trash" style="color: crimson;"></i>
  2247.         Blacklist all post images`);
  2248.    
  2249.         $(postBlacklistButton).click(function(e) {
  2250.           e.preventDefault();
  2251.           $(post).hide();
  2252.           this.blacklistPostImages(post);
  2253.           this.saveBlacklist();
  2254.         }.bind(this));
  2255.    
  2256.         $(post).find(EightKun.POST_MODIFIED_SELECTOR).append(postBlacklistButton);
  2257.    
  2258.         $(post).find(ImageBlacklist.POST_IMG_SELECTOR).each(function(i, img) {
  2259.           const imgBlacklistButton = document.createElement('button');
  2260.           $(imgBlacklistButton).addClass(this.imgBlacklistButtonClass);
  2261.           $(imgBlacklistButton).append(`
  2262.           <i class="fa fa-trash" style="color: crimson;"
  2263.             title="Blacklist image"
  2264.             ></i>`);
  2265.    
  2266.           $(img)
  2267.               .parents('div.file')
  2268.               .find('.fileinfo')
  2269.               .prepend(imgBlacklistButton);
  2270.    
  2271.           $(imgBlacklistButton).click(function(e) {
  2272.             e.preventDefault();
  2273.             const md5 = img.getAttribute('data-md5');
  2274.             this.addToBlacklist(md5);
  2275.             this.deletePostImage(img);
  2276.             this.saveBlacklist();
  2277.           }.bind(this));
  2278.         }.bind(this));
  2279.       }
  2280.    
  2281.       /**
  2282.        * Delete post image
  2283.        * @param {Element} image
  2284.        */
  2285.       deletePostImage(image) {
  2286.         const imageParent = $(image).parent().parent();
  2287.         $(imageParent).append(`
  2288.           Image blacklisted
  2289.           ${image.getAttribute('data-md5')}
  2290.         `);
  2291.         $(image).remove();
  2292.       }
  2293.     }
  2294.     ImageBlacklist.POST_IMG_SELECTOR = 'img.post-image';
  2295.     ImageBlacklist.HIDE_POST_BLACKLIST_BUTTON_SETTING =
  2296.       'bakertools-hide-post-blacklist-button';
  2297.    
  2298.     /* global $, EightKun */
  2299.     /**
  2300.     * Add notable button to posts that opens quick reply
  2301.     * and populates with a template message
  2302.     */
  2303.     class NominatePostButtons {
  2304.       /**
  2305.        * Construct NPB object and setup listeners
  2306.        */
  2307.       constructor() {
  2308.         this.nominateButtonClass = 'bakertools-nominate-button';
  2309.         this.hidePostNotableButtonCheckboxId = 'bakertools-hide-notables';
  2310.         this.bakerNotableHeader = '==BAKER NOTABLE==\n';
  2311.         this.notableReasonPlaceholder = '[REASON FOR NOTABLE HERE]';
  2312.    
  2313.         $('div.post.reply').each(function(i, post) {
  2314.           this._addButtonToPost(post);
  2315.         }.bind(this));
  2316.    
  2317.    
  2318.         this._setupBakerWindowControls();
  2319.         this._setupListeners();
  2320.         this._readSettings();
  2321.       }
  2322.    
  2323.       /**
  2324.        * Read settings from localStorage
  2325.        */
  2326.       _readSettings() {
  2327.         this.showNotableNominationButton(JSON.parse(
  2328.             localStorage.getItem(
  2329.                 NominatePostButtons.HIDE_NOMINATE_BUTTON_SETTING),
  2330.         ));
  2331.       }
  2332.    
  2333.       /**
  2334.        * Add options to baker window
  2335.        */
  2336.       _setupBakerWindowControls() {
  2337.         window.bakerTools.mainWindow.addNotableOption(`
  2338.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2339.           <label for="${this.hidePostNotableButtonCheckboxId}"
  2340.             title="Hide post 'Notable' buttons" >
  2341.             Hide "Notable" buttons
  2342.           </label>
  2343.           <input type="checkbox" id="${this.hidePostNotableButtonCheckboxId}"
  2344.             title="Hide post 'Notable' buttons" /><br />
  2345.         </div>
  2346.         `);
  2347.       }
  2348.    
  2349.       /**
  2350.        * Setup event listeners
  2351.        */
  2352.       _setupListeners() {
  2353.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  2354.           this._addButtonToPost(post);
  2355.         }.bind(this));
  2356.    
  2357.         $('#'+this.hidePostNotableButtonCheckboxId).change(function(e) {
  2358.           this.showNotableNominationButton(e.target.checked);
  2359.         }.bind(this));
  2360.       }
  2361.    
  2362.       /**
  2363.        * Show or hide the notable nomination buttons
  2364.        *
  2365.        * @param {boolean} showNotableNominationButton
  2366.        */
  2367.       showNotableNominationButton(showNotableNominationButton) {
  2368.         $('#'+this.hidePostNotableButtonCheckboxId).prop('checked',
  2369.             showNotableNominationButton);
  2370.    
  2371.         localStorage.setItem(NominatePostButtons.HIDE_NOMINATE_BUTTON_SETTING,
  2372.             showNotableNominationButton);
  2373.    
  2374.         const styleId = 'baker-tools-notable-button-style';
  2375.         if (showNotableNominationButton) {
  2376.           $('head').append(`
  2377.             <style id='${styleId}'>
  2378.               .${this.nominateButtonClass} {
  2379.                 display: none;
  2380.               }
  2381.           `);
  2382.         } else {
  2383.           $(`#${styleId}`).remove();
  2384.         }
  2385.       }
  2386.    
  2387.    
  2388.       /**
  2389.        * Add button to the provided post
  2390.        * @param {Element} post
  2391.        */
  2392.       _addButtonToPost(post) {
  2393.         const button = document.createElement('button');
  2394.         $(button).addClass(this.nominateButtonClass);
  2395.         $(button).append(`
  2396.           <i class="fa fa-star" style="color: goldenrod;"></i>
  2397.           Notable`);
  2398.    
  2399.         $(button).click(function(e) {
  2400.           const postNumber = $(post)
  2401.               .find('.intro .post_no')
  2402.               .text()
  2403.               .replace('No.', '');
  2404.           const href = $(post)
  2405.               .find('.intro .post_no')
  2406.               .get(0).href;
  2407.    
  2408.           // 8kun core - adds >>postnumber to- and unhides quickreply
  2409.           window.citeReply(postNumber, href);
  2410.    
  2411.           const quickReplyBody = $('#quick-reply #body');
  2412.           const oldText = quickReplyBody.val();
  2413.    
  2414.           quickReplyBody.val(oldText + this.bakerNotableHeader +
  2415.             this.notableReasonPlaceholder);
  2416.    
  2417.           // Don't ask me why i have to do this, ask CodeMonkeyZ
  2418.           // Not sure why citeReply which calls cite needs to set a timeout to
  2419.           // replace the body of the quickreply with itself.  We need to combat
  2420.           // that here
  2421.           // setTimeout(function() {
  2422.           //     var tmp = $('#quick-reply textarea[name="body"]').val();
  2423.           //     $('#quick-reply textarea[name="body"]').val('').focus().val(tmp);
  2424.           //  }, 1);
  2425.           // $(window).on('cite', function(e, id, with_link) {
  2426.           // TODO: Figure this out
  2427.           const self = this;
  2428.           setTimeout(function() {
  2429.             quickReplyBody.select();
  2430.             quickReplyBody.prop('selectionStart',
  2431.                 oldText.length + self.bakerNotableHeader.length);
  2432.           }, 1.2);
  2433.         }.bind(this));
  2434.    
  2435.         $(post).find(EightKun.POST_MODIFIED_SELECTOR).append(button);
  2436.       }
  2437.     }
  2438.     NominatePostButtons.HIDE_NOMINATE_BUTTON_SETTING =
  2439.         'bakertools-hide-nominate-button';
  2440.    
  2441.     /* global $, EightKun, ResearchBread,  NotablePost, NavigationControl,
  2442.      ColorPicker, POST_BACKGROUND_CHANGE_EVENT */
  2443.     /**
  2444.     * Makes notable posts easier to see by highlighting posts that anons nominate
  2445.     * as notable.
  2446.     *
  2447.     * If someone replies to a post and their post contains the word 'notable',
  2448.     * the replied to post will be considered notable.
  2449.     *
  2450.     * Both the notable post and the nominator posts will be highlighted, as well
  2451.     * as the nominator link in the notable's mentions will be highlighted.
  2452.     */
  2453.     class NotableHighlighter {
  2454.       /**
  2455.        * Construct notablehighlighter object, find and highlight
  2456.        * current notable sand setup listeners
  2457.        */
  2458.       constructor() {
  2459.         this.styleId = 'bakertools-notable-style';
  2460.         this.NOMINATING_REGEX = /notable/i;
  2461.    
  2462.         this.showOnlyNotablesCheckboxId = 'bakertools-show-only-notable';
  2463.         this.createNotablePostButtonId = 'bakertools-create-notable-post';
  2464.         this.notableEditorId = 'bakertools-notable-editor';
  2465.         this.showNotableNavigationInBoardListId =
  2466.             'bakertools-show-notable-nav-in-boardlist';
  2467.    
  2468.         this._createStyles();
  2469.         this._setupBakerWindowControls();
  2470.         this._readSettings();
  2471.    
  2472.         this.findNominatedNotables();
  2473.         this._setupListeners();
  2474.       }
  2475.    
  2476.       /**
  2477.        * Read settings from local storage
  2478.        */
  2479.       _readSettings() {
  2480.         this.setOnlyShowNotables(JSON.parse(
  2481.             localStorage.getItem(
  2482.                 NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING),
  2483.         ));
  2484.         this.showNotableNavigationInBoardList(JSON.parse(
  2485.             localStorage
  2486.                 .getItem(NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING),
  2487.         ));
  2488.       }
  2489.    
  2490.       /**
  2491.        * Create styles that determine how notables are highlighted
  2492.        */
  2493.       _createStyles() {
  2494.         const nominatorSelector =
  2495.           `${EightKun.POST_REPLY_SELECTOR}.${NotableHighlighter.NOMINATOR_CLASS}`;
  2496.         const notableSelector =
  2497.           `.thread ${EightKun.POST_REPLY_SELECTOR}` +
  2498.           `.${NotableHighlighter.NOTABLE_CLASS}`;
  2499.         $('head').append(`
  2500.         <style id='${this.styleId}'>
  2501.         ${notableSelector} {
  2502.           background-color: ${this.notableColor};
  2503.         }
  2504.         /* less specificity than notable so it has less preference */
  2505.         ${nominatorSelector} {  
  2506.           background-color: ${this.nominatorColor};  
  2507.         }
  2508.         div.post.reply .mentioned .${NotableHighlighter.NOMINATOR_CLASS} {
  2509.           color: ${this.nominatorMentionLinkColor};
  2510.           font-weight: bold;
  2511.           font-size: 1.5em;
  2512.         }
  2513.         ${notableSelector}.highlighted {
  2514.           background: #d6bad0;
  2515.         }
  2516.         ${nominatorSelector}.highlighted {
  2517.           background: #d6bad0;
  2518.         }
  2519.         </style>
  2520.         `);
  2521.       }
  2522.    
  2523.       /**
  2524.        * Add controls to the bakerwindow
  2525.        */
  2526.       _setupBakerWindowControls() {
  2527.         const notablePostsTitle = `Only show, notables, nominators, q, q replied
  2528.           posts`;
  2529.    
  2530.         const notableColorPicker = new ColorPicker(
  2531.             'Notable Post Color',
  2532.             'Set background color of notable Posts',
  2533.             NotableHighlighter.NOTABLE_COLOR_SETTTING,
  2534.             NotableHighlighter.DEFAULT_NOTABLE_COLOR,
  2535.             (color) => this.notableColor = color,
  2536.         );
  2537.         const nominatorColorPicker = new ColorPicker(
  2538.             'Nominator Color',
  2539.             'Set background color of nominator posts',
  2540.             NotableHighlighter.NOMINATOR_COLOR_SETTTING,
  2541.             NotableHighlighter.DEFAULT_NOMINATOR_COLOR,
  2542.             (color) => this.nominatorColor = color,
  2543.         );
  2544.         const nominatorMentionLinkColorPicker = new ColorPicker(
  2545.             'Nominator Mention Link Color',
  2546.             'Set color of nominator mention links',
  2547.             NotableHighlighter.NOMINATOR_MENTION_LINK_COLOR_SETTTING,
  2548.             NotableHighlighter.DEFAULT_NOMINATOR_MENTION_LINK_COLOR,
  2549.             (color) => this.nominatorMentionLinkColor = color,
  2550.         );
  2551.    
  2552.         window.bakerTools.mainWindow.addColorOption(notableColorPicker.element);
  2553.         window.bakerTools.mainWindow.addColorOption(nominatorColorPicker.element);
  2554.         window.bakerTools.mainWindow
  2555.             .addColorOption(nominatorMentionLinkColorPicker.element);
  2556.    
  2557.         window.bakerTools.mainWindow.addNotableOption(`
  2558.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2559.           <label for="${this.showOnlyNotablesCheckboxId}"
  2560.             title="${notablePostsTitle}" >
  2561.             Only Show Notable/Nomination Posts:
  2562.           </label>
  2563.           <input type="checkbox" id="${this.showOnlyNotablesCheckboxId}"
  2564.             title="${notablePostsTitle}" />
  2565.         </div>
  2566.         `);
  2567.    
  2568.    
  2569.         window.bakerTools.mainWindow.addBaker(`
  2570.         <button type="button" id="${this.createNotablePostButtonId}"
  2571.           title="Create notables list post based on current nominated notables" >
  2572.           Create Notable Post
  2573.         </button>
  2574.         <textarea id="${this.notableEditorId}"></textarea>
  2575.         `);
  2576.    
  2577.         window.bakerTools.mainWindow
  2578.             .addNavigationOption(`
  2579.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2580.           <label for="${this.showNotableNavigationInBoardListId}"
  2581.             title="Show navigation controls in board list bar" >
  2582.             Show Notable Nav in Board List:
  2583.           </label>
  2584.           <input type="checkbox" id="${this.showNotableNavigationInBoardListId}"
  2585.             title="Show navigation controls in board list bar" /><br />
  2586.         </div>
  2587.         `);
  2588.    
  2589.         this.navigation = new NavigationControl('Notables',
  2590.             () => NotablePost.getNotablesAsPosts(),
  2591.             NotablePost.NEW_NOTABLE_POST_EVENT);
  2592.         window.bakerTools.mainWindow
  2593.             .addNavigation(this.navigation.element);
  2594.    
  2595.         this.boardListNav = new NavigationControl('Notables',
  2596.             () => NotablePost.getNotablesAsPosts(),
  2597.             NotablePost.NEW_NOTABLE_POST_EVENT);
  2598.    
  2599.         $(EightKun.getTopBoardlist()).append(this.boardListNav.element);
  2600.         $(this.boardListNav.element).hide();
  2601.       }
  2602.    
  2603.       /**
  2604.        * Set the background color of notable posts
  2605.        * @param {string} color A valid css color value.
  2606.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  2607.        */
  2608.       set notableColor(color) {
  2609.         this._notableColor = color;
  2610.         document.getElementById(this.styleId)
  2611.             .sheet.cssRules[0].style.background = color;
  2612.         $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  2613.       }
  2614.    
  2615.       /**
  2616.        * Get color for notable post backgrounds
  2617.        */
  2618.       get notableColor() {
  2619.         return this._notableColor;
  2620.       }
  2621.    
  2622.       /**
  2623.        * Set the background color of nominator posts
  2624.        * @param {string} color A valid css color value.
  2625.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  2626.        */
  2627.       set nominatorColor(color) {
  2628.         this._nominatorColor = color;
  2629.         document.getElementById(this.styleId)
  2630.             .sheet.cssRules[1].style.background = color;
  2631.         $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  2632.       }
  2633.    
  2634.       /**
  2635.        * Get color for notable post backgrounds
  2636.        */
  2637.       get nominatorColor() {
  2638.         return this._nominatorColor;
  2639.       }
  2640.    
  2641.       /**
  2642.        * Set the color of nominator mention links posts
  2643.        * @param {string} color A valid css color value.
  2644.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  2645.        */
  2646.       set nominatorMentionLinkColor(color) {
  2647.         this._nominatorMentionLinkColor = color;
  2648.         document.getElementById(this.styleId)
  2649.             .sheet.cssRules[2].style.color = color;
  2650.       }
  2651.    
  2652.       /**
  2653.        * Get color for notable post backgrounds
  2654.        */
  2655.       get nominatorMentionLinkColor() {
  2656.         return this._nominatorMentionLinkColor;
  2657.       }
  2658.    
  2659.       /**
  2660.        * Setup listeners for new posts, bakerwindow controls, etc
  2661.        */
  2662.       _setupListeners() {
  2663.         $('#'+this.showOnlyNotablesCheckboxId).change(function(e) {
  2664.           this.setOnlyShowNotables(e.target.checked);
  2665.         }.bind(this));
  2666.    
  2667.         $('#'+this.createNotablePostButtonId).click(function() {
  2668.           if ($('#'+this.notableEditorId).val()) {
  2669.             if (!confirm(`If you continue, any changes you made will be
  2670.                 overwritten!`)) {
  2671.               return;
  2672.             }
  2673.           }
  2674.           $('#'+this.notableEditorId).val(this.createNotablesPost());
  2675.         }.bind(this));
  2676.    
  2677.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  2678.           this.checkNewPostsForNotables(post);
  2679.         }.bind(this));
  2680.    
  2681.         $('#'+this.showNotableNavigationInBoardListId).change(function(e) {
  2682.           this.showNotableNavigationInBoardList(e.target.checked);
  2683.         }.bind(this));
  2684.       }
  2685.    
  2686.       /**
  2687.        * Show or hide notable nav control in the boardlist
  2688.        *
  2689.        * @param {boolean} show
  2690.        */
  2691.       showNotableNavigationInBoardList(show) {
  2692.         $('#'+this.showNotableNavigationInBoardListId).prop('checked',
  2693.             show);
  2694.    
  2695.         localStorage
  2696.             .setItem(NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING,
  2697.                 show);
  2698.    
  2699.         if (show) {
  2700.           $(this.boardListNav.element).show();
  2701.         } else {
  2702.           $(this.boardListNav.element).hide();
  2703.         }
  2704.       }
  2705.    
  2706.       /**
  2707.        * Create the notables post for review
  2708.        * @return {string} Returns the notable post string
  2709.        */
  2710.       createNotablesPost() {
  2711.         const notables = NotablePost.getNotables();
  2712.         const breadNumber = ResearchBread.getBreadNumber();
  2713.         let post = `'''#${breadNumber}'''\n\n`;
  2714.    
  2715.         notables.forEach(function(notable) {
  2716.           post += `${notable.shortLink()} ${notable.description}\n\n`;
  2717.         });
  2718.    
  2719.         return post;
  2720.       }
  2721.    
  2722.       /**
  2723.        * Checks a post for notable nominations
  2724.        * @param {Element} post
  2725.        */
  2726.       checkNewPostsForNotables(post) {
  2727.         $(post).removeAttr('style'); // TODO: try removing
  2728.    
  2729.         if (this.isNominatingPost(post)) {
  2730.           NotablePost.fromNominatingPost(post);
  2731.         }
  2732.       }
  2733.    
  2734.       /**
  2735.        * Finds posts that are being tagged as notable.
  2736.        *
  2737.        * I.E. Finding any post that has been replied to by a post with the string
  2738.        * "notable" in it. Maybe at somepoint this can be smarter.  Q give me some
  2739.        * dwave snow white tech!
  2740.        *
  2741.        * Highlights notable posts in yellow
  2742.        * Highlights nominating posts in pink <3
  2743.        * Highlights nominating posts in mentions
  2744.        * Add nominee count to post
  2745.        * @return {Array<NotablePost>}
  2746.        */
  2747.       findNominatedNotables() {
  2748.         const postsWithoutDough = ResearchBread.getPostsWithoutDough();
  2749.    
  2750.         // ^s to ignore notables review posts
  2751.         const nominatingPosts = postsWithoutDough
  2752.             .filter((post) => this.isNominatingPost(post));
  2753.    
  2754.         nominatingPosts.forEach(function(nominatingPost) {
  2755.           NotablePost.fromNominatingPost(nominatingPost);
  2756.         });
  2757.         console.log(NotablePost.getNotables());
  2758.         return NotablePost.getNotables();
  2759.       }
  2760.    
  2761.       /**
  2762.        * Is the post nominating a notable
  2763.        * @arg {Element} post .post
  2764.        * @return {boolean} True if post nominates a notable
  2765.        */
  2766.       isNominatingPost(post) {
  2767.         const postContainsNotable = post.textContent
  2768.             .search(this.NOMINATING_REGEX) != -1;
  2769.         const postIsReplying = EightKun.getReplyLinksFromPost(post).length;
  2770.         return postContainsNotable && postIsReplying;
  2771.       }
  2772.    
  2773.       /**
  2774.        * Toggle whether only the notable/nominee posts are shown or not
  2775.        * @arg {boolean} onlyShowNotables boolean If true, only show
  2776.        *               notables/nominators, else show all
  2777.        */
  2778.       setOnlyShowNotables(onlyShowNotables) {
  2779.         $('#'+this.showOnlyNotablesCheckboxId).prop('checked', onlyShowNotables);
  2780.    
  2781.         localStorage.setItem(NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING,
  2782.             onlyShowNotables);
  2783.    
  2784.         const notableOrNominationPostsSelector =
  2785.           `div.post.${NotableHighlighter.NOTABLE_CLASS},
  2786.           div.post.${NotableHighlighter.NOMINATOR_CLASS}`;
  2787.         const notableOrNominationPostBreaksSelector =
  2788.           `div.post.${NotableHighlighter.NOTABLE_CLASS}+br,
  2789.           div.post.${NotableHighlighter.NOMINATOR_CLASS}+br`;
  2790.         const onlyShowNotablesStyleId = 'bakertools-only-show-notables';
  2791.    
  2792.         if (onlyShowNotables) {
  2793.           $(`<style id='${onlyShowNotablesStyleId}' type='text/css'>
  2794.             div.reply:not(.post-hover),
  2795.             div.post+br {
  2796.               display: none !important;
  2797.               visibility: hidden !important;
  2798.             }
  2799.             ${notableOrNominationPostsSelector},
  2800.             ${notableOrNominationPostBreaksSelector} {
  2801.               display: inline-block !important;
  2802.               visibility: visible !important;
  2803.             }
  2804.             </style>`).appendTo('head');
  2805.         } else {
  2806.           $(`#${onlyShowNotablesStyleId}`).remove();
  2807.           // For whatever reason, when the non notable posts are filtered and new
  2808.           // posts come through the auto_update, the posts are created with
  2809.           // style="display:block" which messes up display.  Remove style attr
  2810.           // TODO: can we remove this now that we have !important?
  2811.           $(EightKun.POST_SELECTOR).removeAttr('style');
  2812.         }
  2813.       }
  2814.    
  2815.       /**
  2816.        * Retrieves only show notable ssetting from localStorage
  2817.        * @return {boolean} true if only show notables is turned on
  2818.        */
  2819.       getOnlyShowNotables() {
  2820.         return localStorage
  2821.             .getItem(NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING);
  2822.       }
  2823.     }
  2824.     NotableHighlighter.NOMINATOR_CLASS = 'bakertools-notable-nominator';
  2825.     NotableHighlighter.NOTABLE_CLASS = 'bakertools-notable';
  2826.     NotableHighlighter.ONLY_SHOW_NOTABLES_SETTING =
  2827.         'bakertools-only-show-notables';
  2828.     NotableHighlighter.SHOW_NOTABLE_NAV_IN_BOARDLIST_SETTING =
  2829.         'bakertools-show-notable-nav-in-boardlist';
  2830.     NotableHighlighter.NOMINATOR_COLOR_SETTTING =
  2831.         'bakertools-nominator-color';
  2832.     NotableHighlighter.NOTABLE_COLOR_SETTTING =
  2833.         'bakertools-notable-color';
  2834.     NotableHighlighter.NOMINATOR_MENTION_LINK_COLOR_SETTTING =
  2835.         'bakertools-nominator-metion-link-color';
  2836.     NotableHighlighter.DEFAULT_NOTABLE_COLOR = '#E5FFCC';
  2837.     NotableHighlighter.DEFAULT_NOMINATOR_COLOR = '#ACC395';
  2838.     NotableHighlighter.DEFAULT_NOMINATOR_MENTION_LINK_COLOR = '#00CC00';
  2839.    
  2840.     /* globals $, EightKun, debounce, BakerWindow */
  2841.     /* exported PostRateChart */
  2842.     /**
  2843.      * Displays chart of post/min
  2844.      */
  2845.     class PostRateChart {
  2846.       /**
  2847.        * Construct a postrate chart
  2848.        */
  2849.       constructor() {
  2850.         this.containerClass = 'bakertools-postrate-container';
  2851.         this.chartClass = 'bakertools-postrate-chart';
  2852.         this.rateClass = 'bakertools-postrate-rate';
  2853.         this.styleId = 'bakertools-postrate-style';
  2854.         this.hidePostRateChartId = 'bakertools-postrate-hide-postrate';
  2855.         this.numberOfPostsForAverage = 10;
  2856.         this.numberOfDataPointsShownOnChart = 10;
  2857.         this.postTimes = [];
  2858.         this.postsPerMinuteHistory = [];
  2859.         this._setupStyles();
  2860.         this._setupBakerWindowControls();
  2861.         this._createElement();
  2862.         this._getExistingPostRates();
  2863.         this._setupListeners();
  2864.         this.draw();
  2865.         this.draw = debounce(this.draw, 1000 *2);
  2866.         this._readSettings();
  2867.       }
  2868.    
  2869.       /**
  2870.        * Read settings from local storage
  2871.        */
  2872.       _readSettings() {
  2873.         const hidePostRate = JSON.parse(localStorage
  2874.             .getItem(PostRateChart.HIDE_POSTRATE_SETTING));
  2875.    
  2876.         this.showPostRateChart(!hidePostRate);
  2877.       }
  2878.    
  2879.       /**
  2880.        * Setup chart styles
  2881.        */
  2882.       _setupStyles() {
  2883.         $('head').append(`
  2884.         <style id='${this.styleId}'>
  2885.         .${this.containerClass} {
  2886.           height: 20px;
  2887.           padding: 0;
  2888.           color: rgb(20, 137, 183);
  2889.         }
  2890.    
  2891.         .${this.chartClass} {
  2892.           border: 1px solid;
  2893.           vertical-align: middle;
  2894.           padding: 1px;
  2895.         }
  2896.    
  2897.         ${EightKun.BOARDLIST_SELECTOR} .${this.containerClass}:before {
  2898.           content: '[';
  2899.           color: #89A;
  2900.         }
  2901.    
  2902.         ${EightKun.BOARDLIST_SELECTOR} .${this.containerClass}:after {
  2903.           content: ']';
  2904.           color: #89A;
  2905.         }
  2906.         `);
  2907.       }
  2908.    
  2909.       /**
  2910.        * Add controls to the bakerwindow
  2911.        */
  2912.       _setupBakerWindowControls() {
  2913.         window.bakerTools.mainWindow.addOption(`
  2914.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  2915.           <label for="${this.hidePostRateChartId}"
  2916.             title="Hide the postrate chart in boardlist" >
  2917.             Hide Post/Min Chart:
  2918.           </label>
  2919.           <input type="checkbox" id="${this.hidePostRateChartId}"
  2920.             title="Hide the postrate chart in boardlist" />
  2921.         </div>
  2922.         `);
  2923.       }
  2924.    
  2925.    
  2926.       /**
  2927.        * Setup listener to record post times
  2928.        */
  2929.       _setupListeners() {
  2930.         $(document).on(EightKun.NEW_POST_EVENT, function(idx, post) {
  2931.           this._addDataPointFromPost(post);
  2932.           this.draw();
  2933.         }.bind(this));
  2934.    
  2935.         $('#'+this.hidePostRateChartId).change(function(e) {
  2936.           this.showPostRateChart(!e.target.checked);
  2937.         }.bind(this));
  2938.       }
  2939.    
  2940.       /**
  2941.        * Show or hide post rate chart in the boardlist
  2942.        *
  2943.        * @param {boolean} show
  2944.        */
  2945.       showPostRateChart(show) {
  2946.         $('#'+this.hidePostRateChartId).prop('checked',
  2947.             !show);
  2948.    
  2949.         localStorage
  2950.             .setItem(PostRateChart.HIDE_POSTRATE_SETTING,
  2951.                 !show);
  2952.    
  2953.         if (show) {
  2954.           $(this.element).show();
  2955.         } else {
  2956.           $(this.element).hide();
  2957.         }
  2958.       }
  2959.    
  2960.       /**
  2961.        * Create the canvas element
  2962.        */
  2963.       _createElement() {
  2964.         this.element = $(`
  2965.         <span class='${this.containerClass}' title = 'Posts Per Minute'>
  2966.           <span class='${this.rateClass}'>0</span> ppm
  2967.           <canvas class='${this.chartClass}'></canvas>
  2968.         </span>`,
  2969.         ).get(0);
  2970.    
  2971.         $(EightKun.getTopBoardlist()).append(this.element);
  2972.    
  2973.         this.canvas = $(this.element).find('canvas').get(0);
  2974.    
  2975.         this.canvas.height = 10;
  2976.         this.canvas.width = 100;
  2977.       }
  2978.    
  2979.       /**
  2980.        * Collect post rate data on posts at page load
  2981.        */
  2982.       _getExistingPostRates() {
  2983.         $('div.post').each(function(idx, post) {
  2984.           this._addDataPointFromPost(post);
  2985.         }.bind(this));
  2986.       }
  2987.    
  2988.       /**
  2989.        * Add a data point (aka the time a post was made)
  2990.        * @param {Element} post div.post
  2991.        */
  2992.       _addDataPointFromPost(post) {
  2993.         this.postTimes.push(EightKun.getPostTime(post));
  2994.         if (this._isEnoughDataToAverage()) {
  2995.           this._recordPostPerMinute();
  2996.         }
  2997.       }
  2998.    
  2999.       /**
  3000.        * Return true if theres enough data to perform averaging
  3001.        * @return {boolean} true if enough data
  3002.        */
  3003.       _isEnoughDataToAverage() {
  3004.         return this.postTimes.length > (this.numberOfPostsForAverage + 1);
  3005.       }
  3006.    
  3007.       /**
  3008.        * Record post per minute with the current set of post times
  3009.        * Calc is done with the last ${this.numberOfPostsForAverage} post times
  3010.        */
  3011.       _recordPostPerMinute() {
  3012.         const startPostIndex =
  3013.           this.postTimes.length - this.numberOfPostsForAverage - 1;
  3014.         const endPostIndex = this.postTimes.length - 1;
  3015.         const startPostTime = this.postTimes[startPostIndex];
  3016.         const endPostTime = this.postTimes[endPostIndex];
  3017.    
  3018.         const postsPerMinute =
  3019.             this.numberOfPostsForAverage / ((endPostTime - startPostTime) / 60);
  3020.         this.postsPerMinuteHistory.push(postsPerMinute);
  3021.       }
  3022.    
  3023.       /**
  3024.        * Draw the post rate chart
  3025.        */
  3026.       draw() {
  3027.         if (!this.postsPerMinuteHistory.length) {
  3028.           return;
  3029.         }
  3030.    
  3031.         const canvas = this.canvas;
  3032.         const ctx = canvas.getContext('2d');
  3033.    
  3034.         this._setPostRateText();
  3035.    
  3036.         const normalizedPostPerMinutes = this._normalizePostPerMinutes();
  3037.    
  3038.         ctx.clearRect(0, 0, canvas.width, canvas.height);
  3039.         ctx.strokeStyle = $(`${EightKun.BOARDLIST_SELECTOR} a`).css('color');
  3040.         ctx.beginPath();
  3041.    
  3042.         let x = 0;
  3043.         let y = canvas.height * normalizedPostPerMinutes[0];
  3044.    
  3045.         ctx.moveTo(x, y);
  3046.         normalizedPostPerMinutes.slice(1).forEach(function(ppm, i) {
  3047.           x = (i+1) * (canvas.width / this.numberOfDataPointsShownOnChart);
  3048.           y = canvas.height * ppm;
  3049.           ctx.lineTo(x, y);
  3050.         }.bind(this));
  3051.         ctx.stroke();
  3052.         ctx.closePath();
  3053.       }
  3054.    
  3055.       /**
  3056.        * Set the text label of current PPM
  3057.        */
  3058.       _setPostRateText() {
  3059.         const lastIndex = this.postsPerMinuteHistory.length-1;
  3060.         const currentPPM =
  3061.           this.postsPerMinuteHistory[lastIndex].toFixed(2);
  3062.         $(`.${this.rateClass}`).text(currentPPM);
  3063.       }
  3064.    
  3065.       /**
  3066.        * Normalize the data points to be within 0-1 range.
  3067.        * Slice the array to only contain the currently drawn slice
  3068.        * @return {Array}
  3069.        */
  3070.       _normalizePostPerMinutes() {
  3071.         const slicedArray =
  3072.           this.postsPerMinuteHistory.slice(-this.numberOfDataPointsShownOnChart);
  3073.         const maxPPM = Math.max(...slicedArray);
  3074.         const minPPM = Math.min(...slicedArray);
  3075.         const range = maxPPM - minPPM;
  3076.         return slicedArray.map(function(ppm) {
  3077.           return (ppm - minPPM) / range;
  3078.         });
  3079.       }
  3080.     }
  3081.     PostRateChart.HIDE_POSTRATE_SETTING =
  3082.       'bakertools-hide-postrate-chart';
  3083.    
  3084.     /* global $, EightKun, ColorPicker */
  3085.     /* exported PreviousBreadHighlighter */
  3086.     /**
  3087.     * Highlights previous bread post links
  3088.     */
  3089.     class PreviousBreadHighlighter {
  3090.       /**
  3091.        * Construct pb highlighter object, setup listeners
  3092.        */
  3093.       constructor() {
  3094.         this.styleId = 'bakertools-previous-bread-styles';
  3095.         this.previousBreadClass = 'bakertools-PreviousBread';
  3096.         this.newerBreadClass = 'bakertools-NewBread';
  3097.         this._linkSelector = 'div.body > p.body-line.ltr > a';
  3098.    
  3099.         this._setupStyles();
  3100.         this._setupBakerWindowControls();
  3101.    
  3102.         const links = $(this._linkSelector).filter('[onClick]');
  3103.         links.each(function(index, link) {
  3104.           this.markLinkIfPreviousBread(link);
  3105.         }.bind(this));
  3106.    
  3107.         this._setupListeners();
  3108.       }
  3109.    
  3110.       /**
  3111.        * Setup color picker controls
  3112.        */
  3113.       _setupBakerWindowControls() {
  3114.         const colorPicker = new ColorPicker(
  3115.             'Previous Bread Link Color',
  3116.             'Set the color of links to previous breads',
  3117.             PreviousBreadHighlighter.PREVIOUS_BREAD_LINK_COLOR_SETTING,
  3118.             PreviousBreadHighlighter.DEFAULT_PREVIOUS_BREAD_LINK_COLOR,
  3119.             (color) => this.previousBreadLinkColor = color,
  3120.         );
  3121.    
  3122.         window.bakerTools.mainWindow.addColorOption(colorPicker.element);
  3123.    
  3124.         const newerBreadColorPicker = new ColorPicker(
  3125.             'Newer Bread Link Color',
  3126.             'Set the color of links to newer breads',
  3127.             PreviousBreadHighlighter.NEWER_BREAD_LINK_COLOR_SETTING,
  3128.             PreviousBreadHighlighter.DEFAULT_NEWER_BREAD_LINK_COLOR,
  3129.             (color) => this.newerBreadLinkColor = color,
  3130.         );
  3131.    
  3132.         window.bakerTools.mainWindow.addColorOption(newerBreadColorPicker.element);
  3133.       }
  3134.    
  3135.       /**
  3136.        * Set the  color of pb links
  3137.        * @param {string} color A valid css color value.
  3138.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3139.        */
  3140.       set previousBreadLinkColor(color) {
  3141.         this._previousBreadLinkColor = color;
  3142.         document.getElementById(this.styleId)
  3143.             .sheet.cssRules[0].style.color = color;
  3144.       }
  3145.    
  3146.       /**
  3147.        * Get color of pb links
  3148.        */
  3149.       get previousBreadLinkColor() {
  3150.         return this._previousBreadLinkColor;
  3151.       }
  3152.    
  3153.       /**
  3154.        * Set the  color of nb links
  3155.        * @param {string} color A valid css color value.
  3156.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3157.        */
  3158.       set newerBreadLinkColor(color) {
  3159.         this._newerBreadLinkColor = color;
  3160.         document.getElementById(this.styleId)
  3161.             .sheet.cssRules[2].style.color = color;
  3162.       }
  3163.    
  3164.       /**
  3165.        * Get color of nb links
  3166.        */
  3167.       get newerBreadLinkColor() {
  3168.         return this._newerBreadLinkColor;
  3169.       }
  3170.    
  3171.       /**
  3172.        * Setup styles for pb links
  3173.        */
  3174.       _setupStyles() {
  3175.         $('head').append(`
  3176.         <style id='${this.styleId}'>
  3177.         ${EightKun.POST_REPLY_SELECTOR} div.body a.${this.previousBreadClass} {
  3178.             color: ${this.previousBreadLinkColor};
  3179.         }
  3180.         a.${this.previousBreadClass}::after {
  3181.           content: " (pb)";
  3182.         }
  3183.    
  3184.         ${EightKun.POST_REPLY_SELECTOR} div.body a.${this.newerBreadClass} {
  3185.             color: ${this.newerBreadLinkColor};
  3186.         }
  3187.         a.${this.newerBreadClass}::after {
  3188.           content: " (nb)";
  3189.         }
  3190.         </style>
  3191.         `);
  3192.       }
  3193.    
  3194.       /**
  3195.        * Setup listeners for pb highlighting
  3196.        */
  3197.       _setupListeners() {
  3198.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  3199.           $(post).find(this._linkSelector)
  3200.               .each((index, link) => this.markLinkIfPreviousBread(link));
  3201.         }.bind(this));
  3202.       }
  3203.    
  3204.       /**
  3205.        * Marks the link if it is pb
  3206.        *
  3207.        * @param {Anchor} link
  3208.        */
  3209.       markLinkIfPreviousBread(link) {
  3210.         const currentBreadNumber = document.location.pathname
  3211.             .split('/')
  3212.             .slice(-1)[0]
  3213.             .split('.')[0];
  3214.         const linkBreadNumber = link.href.split('/')
  3215.             .slice(-1)[0]
  3216.             .split('#')[0]
  3217.             .split('.')[0];
  3218.    
  3219.         const isAReplyLink = $(link)
  3220.             .attr('onclick')
  3221.             .search(EightKun.REPLY_REGEX) != 1;
  3222.         if (isAReplyLink &&
  3223.             parseInt(currentBreadNumber, 10) > parseInt(linkBreadNumber, 10)) {
  3224.           $(link).addClass(this.previousBreadClass);
  3225.         } else if (isAReplyLink &&
  3226.           parseInt(currentBreadNumber, 10) < parseInt(linkBreadNumber, 10)) {
  3227.           $(link).addClass(this.newerBreadClass);
  3228.         }
  3229.       }
  3230.     }
  3231.     PreviousBreadHighlighter.PREVIOUS_BREAD_LINK_COLOR_SETTING =
  3232.       'bakertools-previous-bread-link-color';
  3233.     PreviousBreadHighlighter.DEFAULT_PREVIOUS_BREAD_LINK_COLOR =
  3234.       '#0000CC';
  3235.     PreviousBreadHighlighter.NEWER_BREAD_LINK_COLOR_SETTING =
  3236.       'bakertools-newer-bread-link-color';
  3237.     PreviousBreadHighlighter.DEFAULT_NEWER_BREAD_LINK_COLOR =
  3238.       '#00CC00';
  3239.    
  3240.     /* global $, EightKun, ResearchBread, NavigationControl,
  3241.      ColorPicker, POST_BACKGROUND_CHANGE_EVENT, BakerWindow */
  3242.     /**
  3243.     * Highlight Q posts, replies to q, q replies.
  3244.     * Adds navigation to baker window
  3245.     */
  3246.     class QPostHighlighter {
  3247.       /**
  3248.        * Construct qposthighlighter object and setup listeners
  3249.        */
  3250.       constructor() {
  3251.         this.styleId = 'bakertools-q-style';
  3252.         this.qPostClass = 'bakertools-q-post';
  3253.         this.qReplyClass = 'bakertools-q-reply';
  3254.         this.qMentionClass = 'bakertools-q-mention';
  3255.         this.qLinkClass = 'bakertools-q-link';
  3256.         this.styleId = 'bakertools-q-styles';
  3257.         this._linkSelector = 'div.body > p.body-line.ltr > a';
  3258.         this.showQNavigationInBoardListId =
  3259.             'bakertools-show-q-nav-in-boardlist';
  3260.    
  3261.         this.currentQTripCode = null;
  3262.    
  3263.         this._setupStyles();
  3264.         this._setupBakerWindowControls();
  3265.         this._readSettings();
  3266.         this._findQPosts();
  3267.         this._setupListeners();
  3268.       }
  3269.    
  3270.       /**
  3271.        * Read settings from localStorage
  3272.        */
  3273.       _readSettings() {
  3274.         this.showQNavigationInBoardList(JSON.parse(
  3275.             localStorage
  3276.                 .getItem(QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING),
  3277.         ));
  3278.       }
  3279.    
  3280.       /**
  3281.        * Setup styles for highlighting q posts
  3282.        */
  3283.       _setupStyles() {
  3284.         $('head').append(`
  3285.         <style id='${this.styleId}'>
  3286.          ${EightKun.POST_REPLY_SELECTOR}.${this.qPostClass} {
  3287.           background: ${this.qPostColor};
  3288.           display: inline-block !important;
  3289.           visibility: visible !important;
  3290.         }
  3291.    
  3292.         ${EightKun.POST_REPLY_SELECTOR}.${this.qReplyClass} {
  3293.           background: ${this.qYouColor};
  3294.           display: inline-block !important;
  3295.           visibility: visible !important;
  3296.         }
  3297.    
  3298.         ${EightKun.POST_REPLY_SELECTOR}.${this.qPostClass}.highlighted {
  3299.           background: #d6bad0;
  3300.         }
  3301.    
  3302.         ${EightKun.POST_REPLY_SELECTOR} .intro .${this.qMentionClass},
  3303.         .${this.qLinkClass} {
  3304.           padding:1px 3px 1px 3px;
  3305.           background-color:black;
  3306.           border-radius:8px;
  3307.           border:1px solid #bbbbee;
  3308.           color:gold;
  3309.           background: linear-gradient(300deg, #ff0000, #ff0000, #ff0000, #bbbbbb,
  3310.                     #4444ff);
  3311.           background-size: 800% 800%;
  3312.    
  3313.           -webkit-animation: Patriot 5s ease infinite;
  3314.           -moz-animation: Patriot 5s ease infinite;
  3315.           -o-animation: Patriot 5s ease infinite;
  3316.           animation: Patriot 5s ease infinite;
  3317.           -webkit-text-fill-color: transparent;
  3318.            
  3319.           background: -o-linear-gradient(transparent, transparent);
  3320.           -webkit-background-clip: text;
  3321.         }
  3322.         </style>
  3323.         `);
  3324.       }
  3325.    
  3326.       /**
  3327.        * Set the background color of q posts
  3328.        * @param {string} color A valid css color value.
  3329.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3330.        */
  3331.       set qPostColor(color) {
  3332.         this._qPostColor = color;
  3333.         document.getElementById(this.styleId)
  3334.             .sheet.cssRules[0].style.background = color;
  3335.         $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  3336.       }
  3337.    
  3338.       /**
  3339.        * Get color for q post backgrounds
  3340.        */
  3341.       get qPostColor() {
  3342.         return this._qPostColor;
  3343.       }
  3344.    
  3345.       /**
  3346.        * Set the background color of q posts
  3347.        * @param {string} color A valid css color value.
  3348.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  3349.        */
  3350.       set qYouColor(color) {
  3351.         this._qYouColor = color;
  3352.         document.getElementById(this.styleId)
  3353.             .sheet.cssRules[1].style.background = color;
  3354.         $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  3355.       }
  3356.    
  3357.       /**
  3358.        * Get bg color for posts q replies to
  3359.        */
  3360.       get qYouColor() {
  3361.         return this._qYouColor;
  3362.       }
  3363.    
  3364.       /**
  3365.        * Get Q's current trip code from the bread
  3366.        */
  3367.       _getCurrentQTripFromBread() {
  3368.         const tripCodeMatch = $(EightKun.getOpPost())
  3369.             .text()
  3370.             .match(/Q's Trip-code: Q (.+?\s)/);
  3371.    
  3372.         if (!tripCodeMatch) {
  3373.           console.error('Could not find Q\'s tripcode');
  3374.           return;
  3375.         }
  3376.         this.currentQTripCode = tripCodeMatch[1].split(' ')[0];
  3377.       }
  3378.    
  3379.       /**
  3380.        * Find current Q posts in bread
  3381.        */
  3382.       _findQPosts() {
  3383.         const posts = ResearchBread.getPostsWithoutDough();
  3384.    
  3385.         $(posts).each(function(i, post) {
  3386.           this._doItQ(post);
  3387.         }.bind(this));
  3388.       }
  3389.    
  3390.       /**
  3391.        * Check if the post is Q
  3392.        * WWG1WGA
  3393.        *
  3394.        * @param {Element} post a div.post
  3395.        */
  3396.       _doItQ(post) {
  3397.         if (this._markIfQPost(post)) { // Q Post, lets check for q replies
  3398.           const qPostNumber = $(post)
  3399.               .find('.intro .post_no')
  3400.               .text()
  3401.               .replace('No.', '');
  3402.    
  3403.           const links = $(post)
  3404.               .find(this._linkSelector)
  3405.               .filter('[onClick]');
  3406.    
  3407.           $(links).each(function(i, link) {
  3408.             const postNumber = link.href.split('#')[1];
  3409.             // Enlightened post
  3410.             $(`#reply_${postNumber}`).addClass(this.qReplyClass);
  3411.    
  3412.             const metionLinkSelector = `#reply_${postNumber} .intro .mentioned a`;
  3413.             $(metionLinkSelector).each(function(i, mentionAnchor) {
  3414.               const mentionPostNumber = $(mentionAnchor).text().replace('>>', '');
  3415.               if (mentionPostNumber == qPostNumber) {
  3416.                 $(mentionAnchor).addClass(this.qMentionClass);
  3417.               }
  3418.             }.bind(this));
  3419.           }.bind(this));
  3420.         } else { // Not Q, but lets check if this post replies to Q
  3421.           const links = $(post).find(this._linkSelector).filter('[onClick]');
  3422.    
  3423.           $(links).each(function(i, link) {
  3424.             const postNumber = link.href.split('#')[1];
  3425.             const replyPost = document.querySelector(`#reply_${postNumber}`);
  3426.             // TODO: need to handle pb posts
  3427.             if (this.isQ(replyPost)) {
  3428.               $(link).addClass(this.qLinkClass);
  3429.             }
  3430.           }.bind(this));
  3431.         }
  3432.       }
  3433.    
  3434.       /**
  3435.        * @arg {Element} post div.post.reply
  3436.        * @return {boolean} true if it is a q post
  3437.        */
  3438.       _markIfQPost(post) {
  3439.         let isQ = false;
  3440.         if (this.isQ(post)) {
  3441.           isQ = true;
  3442.           $(post).addClass(this.qPostClass);
  3443.           QPostHighlighter.qPosts.push(post);
  3444.    
  3445.           $(document).trigger(QPostHighlighter.NEW_Q_POST_EVENT, post);
  3446.         }
  3447.         return isQ;
  3448.       }
  3449.    
  3450.       /**
  3451.        * Is the post Q?
  3452.        * @param {Element} post a div.post.reply
  3453.        * @return {boolean} true if the post is Q
  3454.        */
  3455.       isQ(post) {
  3456.         const qTripHistory = QTripCodeHistory.INSTANCE;
  3457.         const dateOfPost = new Date(EightKun.getPostDateTime(post));
  3458.         const expectedQTripBasedOnDate = qTripHistory.getTripCodeByDate(dateOfPost);
  3459.         if (!expectedQTripBasedOnDate) {
  3460.           console.info(`Could not find Q trip code for date: ${dateOfPost}`);
  3461.           return false;
  3462.         }
  3463.    
  3464.         return EightKun.getPostTrip(post) == expectedQTripBasedOnDate.tripCode;
  3465.       }
  3466.    
  3467.       /**
  3468.        * Add Q post navigation to bakerwindow
  3469.        */
  3470.       _setupBakerWindowControls() {
  3471.         window.bakerTools.mainWindow
  3472.             .addNavigationOption(`
  3473.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  3474.           <label for="${this.showQNavigationInBoardListId}"
  3475.             title="Show navigation controls in board list bar" >
  3476.             Show Q Nav in Board List:
  3477.           </label>
  3478.           <input type="checkbox" id="${this.showQNavigationInBoardListId}"
  3479.             title="Show navigation controls in board list bar" /><br />
  3480.         </div>
  3481.         `);
  3482.    
  3483.         this.navigation = new NavigationControl('Q Posts',
  3484.             () => QPostHighlighter.qPosts, QPostHighlighter.NEW_Q_POST_EVENT);
  3485.         window.bakerTools.mainWindow
  3486.             .addNavigation(this.navigation.element);
  3487.    
  3488.         this.boardListNav = new NavigationControl('Q',
  3489.             () => QPostHighlighter.qPosts, QPostHighlighter.NEW_Q_POST_EVENT);
  3490.    
  3491.         $(EightKun.getTopBoardlist()).append(this.boardListNav.element);
  3492.         $(this.boardListNav.element).hide();
  3493.    
  3494.         const qColorPicker = new ColorPicker(
  3495.             'Q Post Color',
  3496.             'Set background color of Q Posts',
  3497.             QPostHighlighter.Q_POST_COLOR_SETTING,
  3498.             QPostHighlighter.DEFAULT_Q_POST_COLOR,
  3499.             (color) => this.qPostColor = color,
  3500.         );
  3501.         const qYouColorPicker = new ColorPicker(
  3502.             'Q (You) Color',
  3503.             'Set background color of posts Q Replies to',
  3504.             QPostHighlighter.Q_YOU_POST_COLOR_SETTING,
  3505.             QPostHighlighter.DEFAULT_Q_YOU_POST_COLOR,
  3506.             (color) => this.qYouColor = color,
  3507.         );
  3508.    
  3509.         window.bakerTools.mainWindow.addColorOption(qColorPicker.element);
  3510.         window.bakerTools.mainWindow.addColorOption(qYouColorPicker.element);
  3511.       }
  3512.    
  3513.    
  3514.       /**
  3515.        * Setup listeners for new posts
  3516.        */
  3517.       _setupListeners() {
  3518.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  3519.           this._doItQ(post);
  3520.         }.bind(this));
  3521.    
  3522.         $('#'+this.showQNavigationInBoardListId).change(function(e) {
  3523.           this.showQNavigationInBoardList(e.target.checked);
  3524.         }.bind(this));
  3525.       }
  3526.    
  3527.       /**
  3528.        * Show or hide q nav control in the boardlist
  3529.        *
  3530.        * @param {boolean} showNavInBoardList
  3531.        */
  3532.       showQNavigationInBoardList(showNavInBoardList) {
  3533.         $('#'+this.showQNavigationInBoardListId).prop('checked',
  3534.             showNavInBoardList);
  3535.    
  3536.         localStorage.setItem(QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING,
  3537.             showNavInBoardList);
  3538.    
  3539.         if (showNavInBoardList) {
  3540.           $(this.boardListNav.element).show();
  3541.         } else {
  3542.           $(this.boardListNav.element).hide();
  3543.         }
  3544.       }
  3545.     }
  3546.     QPostHighlighter.qPosts = [];
  3547.     QPostHighlighter.NEW_Q_POST_EVENT = 'bakertools-new-q-post';
  3548.     QPostHighlighter.SHOW_Q_NAV_IN_BOARDLIST_SETTING =
  3549.         'bakertools-show-q-nav-in-boardlist';
  3550.     QPostHighlighter.Q_YOU_COLOR_SETTING =
  3551.         'bakertools-q-you-color';
  3552.     QPostHighlighter.Q_POST_COLOR_SETTING =
  3553.         'bakertools-q-post-color';
  3554.     QPostHighlighter.DEFAULT_Q_POST_COLOR = '#FFFFCC';
  3555.     QPostHighlighter.DEFAULT_Q_YOU_POST_COLOR = '#DDDDDD';
  3556.    
  3557.     /**
  3558.      * History of Q's tripcodes and their date ranges
  3559.      */
  3560.     class QTripCodeHistory {
  3561.       /**
  3562.        * Construct the q trip history
  3563.        */
  3564.       constructor() {
  3565.         // Hat tip to https://8kun.top/qresearch/res/7762733.html#7832643 for Q trip history
  3566.         this.history = [
  3567.           new QTripCode('!ITPb.qbhqo',
  3568.               new Date('2017-11-10 04:07:15Z'), new Date('2017-12-15 06:04:43Z')),
  3569.           new QTripCode('!UW.yye1fxo',
  3570.               new Date('2017-12-15 06:04:06Z'), new Date('2018-03-24 13:09:02Z')),
  3571.           new QTripCode('!xowAT4Z3VQ',
  3572.               new Date('2018-03-24 13:09:37Z'), new Date('2018-05-04 20:02:22Z')),
  3573.           new QTripCode('!2jsTvXXmXs',
  3574.               new Date('2018-05-04 20:01:19Z'), new Date('2018-05-08 23:46:39Z')),
  3575.           new QTripCode('!4pRcUA0lBE',
  3576.               new Date('2018-05-08 23:47:17Z'), new Date('2018-05-19 22:06:20Z')),
  3577.           new QTripCode('!CbboFOtcZs',
  3578.               new Date('2018-05-19 22:07:06Z'), new Date('2018-08-05 20:12:52Z')),
  3579.           new QTripCode('!A6yxsPKia.',
  3580.               new Date('2018-08-05 20:14:24Z'), new Date('2018-08-10 18:24:24Z')),
  3581.           new QTripCode('!!mG7VJxZNCI',
  3582.               new Date('2018-08-10 18:26:08Z'), new Date('2019-11-25 22:35:45Z')),
  3583.           new QTripCode('!!Hs1Jq13jV6',
  3584.               new Date('2019-12-02 17:55:59Z'), null),
  3585.         ];
  3586.       }
  3587.    
  3588.       /**
  3589.        * Get Q Tripcode by the provided date
  3590.        * @param {Date} date
  3591.        * @return {QTripCode}
  3592.        */
  3593.       getTripCodeByDate(date) {
  3594.         let returnTripCode = null;
  3595.         for (const tripCode of this.history) {
  3596.           if (tripCode.isValidForDate(date)) {
  3597.             returnTripCode = tripCode;
  3598.             break;
  3599.           }
  3600.         }
  3601.         return returnTripCode;
  3602.       }
  3603.    
  3604.       /**
  3605.        * Get Q Tripcode by the current
  3606.        * @return {QTripCode}
  3607.        */
  3608.       getCurrentTripCode() {
  3609.         return this.getTripCodeByDate(new Date());
  3610.       }
  3611.     }
  3612.    
  3613.     /**
  3614.      * Represents a Tripcode used by Q and the timeframe
  3615.      */
  3616.     class QTripCode {
  3617.       /**
  3618.        * Create a new QTripCode
  3619.        * @param {string} tripCode
  3620.        * @param {DateTime} startDate
  3621.        * @param {DateTime} endDate
  3622.        */
  3623.       constructor(tripCode, startDate, endDate) {
  3624.         this.tripCode = tripCode;
  3625.         this.startDate = startDate;
  3626.         this.isCurrentTrip = false;
  3627.         if (!endDate) {
  3628.           this.isCurrentTrip = true;
  3629.         }
  3630.         this.endDate = endDate;
  3631.       }
  3632.    
  3633.       /**
  3634.        * Is this tripcode valid for the provided date?
  3635.        * @param {Date} date
  3636.        * @return {boolean} true if this trip code is valid for the date
  3637.        */
  3638.       isValidForDate(date) {
  3639.         const dateIsOnOrAfterTripStart = date >= this.startDate;
  3640.         const dateIsOnOrBeforeTripEnd = date <= this.endDate;
  3641.         return dateIsOnOrAfterTripStart &&
  3642.           (this.isCurrentTrip || dateIsOnOrBeforeTripEnd);
  3643.       }
  3644.     }
  3645.    
  3646.     QTripCodeHistory.INSTANCE = new QTripCodeHistory();
  3647.    
  3648.     /* globals $, EightKun, ResearchBread, BakerWindow */
  3649.     /* exported SpamFader, NameFagStrategy, HighPostCountFagStrategy,
  3650.     * FloodFagStrategy, BreadShitterFagStrategy */
  3651.    
  3652.     /**
  3653.     * Fade posts that post too fast
  3654.     */
  3655.     class SpamFader {
  3656.       /**
  3657.        * Construct spamfader
  3658.        * @param {Array} spamDetectionStrategies An array of SpamDetectionStrategy's
  3659.        */
  3660.       constructor(spamDetectionStrategies) {
  3661.         this.spamDetectionStrategies = spamDetectionStrategies;
  3662.         this.styleId = 'bakertools-spamfader-style';
  3663.         this.spamClass = 'bakertools-spamfader-spam';
  3664.         this.disableSpamFaderId = 'bakertools-spamfader-disable';
  3665.         this.hideSpamBadgesId = 'bakertools-spamfader-hide-spam-badges';
  3666.         this._createStyles();
  3667.         this._setupBakerWindowControls();
  3668.         this._readSettings();
  3669.         this._spamFadeExistingPosts();
  3670.         this._setupListeners();
  3671.       }
  3672.    
  3673.       /**
  3674.        * Create stylesheets
  3675.        */
  3676.       _createStyles() {
  3677.         $('head').append(`
  3678.         <style id='${this.styleId}'>
  3679.           div.post.post-hover {
  3680.             opacity: 1 !important;
  3681.           }
  3682.         </style>
  3683.         `);
  3684.       }
  3685.    
  3686.       /**
  3687.        * Setup settings UI for spamfading
  3688.        */
  3689.       _setupBakerWindowControls() {
  3690.         window.bakerTools.mainWindow.addSpamOption(`
  3691.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  3692.           <label for='${this.disableSpamFaderId}'>Disable SpamFader</label>
  3693.           <input type='checkbox' id='${this.disableSpamFaderId}' />
  3694.         </div>
  3695.    
  3696.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  3697.           <label for='${this.hideSpamBadgesId}'>Hide spam badges</label>
  3698.           <input type='checkbox' id='${this.hideSpamBadgesId}'/>
  3699.         </div>
  3700.         `);
  3701.       }
  3702.    
  3703.       /**
  3704.        * Loop through posts for spam
  3705.        */
  3706.       _spamFadeExistingPosts() {
  3707.         $(EightKun.POST_REPLY_SELECTOR).each(function(i, post) {
  3708.           this._detectSpam(post);
  3709.         }.bind(this));
  3710.       }
  3711.    
  3712.       /**
  3713.        * Determine if provided post is spam, if so, add spam class
  3714.        *
  3715.        * @param {Element} post div.post
  3716.        */
  3717.       _detectSpam(post) {
  3718.         const posterStats = SpamFader.getPosterStats(post);
  3719.         posterStats.addPost(post);
  3720.    
  3721.         if (SpamFader.isMarkedAsNotSpam(posterStats)) {
  3722.           return;
  3723.         }
  3724.    
  3725.         this.spamDetectionStrategies.forEach((sds) => sds.isSpam(post));
  3726.         this._takeSpamAction(posterStats);
  3727.       }
  3728.    
  3729.       /**
  3730.        * Performs the spam action against the poster's posts.
  3731.        * @param {PosterStats} posterStats
  3732.        */
  3733.       _takeSpamAction(posterStats) {
  3734.         if (!posterStats.isSpam) {
  3735.           return;
  3736.         }
  3737.         if (this.spamAction === SpamFader.FADE) {
  3738.           const opacity =
  3739.             Math.max(SpamFader.MIN_OPACITY, (1 - posterStats.fadeProgress));
  3740.    
  3741.           posterStats.posts.forEach(function(p) {
  3742.             $(p).css('opacity', opacity);
  3743.             $(p).off('mouseenter mouseleave');
  3744.             $(p).hover(function() {
  3745.               $(p).animate({opacity: 1.0}, SpamFader.ANIMATION_DURATION);
  3746.             }, function() {
  3747.               $(p).animate({opacity: opacity}, SpamFader.ANIMATION_DURATION);
  3748.             });
  3749.           });
  3750.         } else if (this.spamAction === SpamFader.HIDE) {
  3751.           posterStats.posts.forEach(function(p) {
  3752.             EightKun.hidePost(p);
  3753.           });
  3754.         }
  3755.       }
  3756.    
  3757.       /**
  3758.        * Setup new post listener
  3759.        */
  3760.       _setupListeners() {
  3761.         this._setupNewPostListener();
  3762.         $(`#${this.disableSpamFaderId}`).change(function(e) {
  3763.           this.disableSpamFader(e.target.checked);
  3764.         }.bind(this));
  3765.    
  3766.         $(`#${this.hideSpamBadgesId}`).change(function(e) {
  3767.           this.hideSpamBadges(e.target.checked);
  3768.         }.bind(this));
  3769.       }
  3770.    
  3771.       /**
  3772.        * Setup listener to check new posts for spam
  3773.        */
  3774.       _setupNewPostListener() {
  3775.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  3776.           this._detectSpam(post);
  3777.         }.bind(this));
  3778.       }
  3779.    
  3780.       /**
  3781.        * Hide the actions of the spamfader.
  3782.        * @param {boolean} disable
  3783.        */
  3784.       disableSpamFader(disable) {
  3785.         $('#'+this.disableSpamFaderId).prop('checked', disable);
  3786.         localStorage.setItem(SpamFader.DISABLE_SPAM_FADER_SETTING, disable);
  3787.    
  3788.         if (disable) {
  3789.           $(SpamFader.SPAM_BADGES_SELECTOR).hide();
  3790.           $(SpamFader.NOT_SPAM_BUTTON_SELECTOR).hide();
  3791.           $(EightKun.POST_REPLY_SELECTOR)
  3792.               .css({'opacity': ''})
  3793.               .off('mouseenter mouseleave');
  3794.    
  3795.           $(document).off(EightKun.NEW_POST_EVENT);
  3796.         } else {
  3797.           $(SpamFader.SPAM_BADGES_SELECTOR).show();
  3798.           $(SpamFader.NOT_SPAM_BUTTON_SELECTOR).show();
  3799.           SpamFader.posterStats.forEach(this._takeSpamAction.bind(this));
  3800.           this._setupNewPostListener();
  3801.         }
  3802.       }
  3803.    
  3804.       /**
  3805.        * Hide spam badges on posts
  3806.        * @param {boolean} hide
  3807.        */
  3808.       hideSpamBadges(hide) {
  3809.         $('#'+this.hideSpamBadgesId).prop('checked', hide);
  3810.    
  3811.         localStorage.setItem(SpamFader.HIDE_SPAM_BADGES_SETTING, hide);
  3812.    
  3813.         if (hide) {
  3814.           $(SpamFader.SPAM_BADGES_SELECTOR).hide();
  3815.         } else {
  3816.           $(SpamFader.SPAM_BADGES_SELECTOR).show();
  3817.         }
  3818.       }
  3819.    
  3820.       /**
  3821.        * Read spamfader settings
  3822.        */
  3823.       _readSettings() {
  3824.         this.spamAction = localStorage[SpamFader.SPAM_ACTION_SETTING] ||
  3825.           SpamFader.FADE;
  3826.    
  3827.         this.hideSpamBadges(JSON.parse(
  3828.             localStorage.getItem(
  3829.                 SpamFader.HIDE_SPAM_BADGES_SETTING),
  3830.         ));
  3831.    
  3832.         this.disableSpamFader(JSON.parse(
  3833.             localStorage.getItem(
  3834.                 SpamFader.DISABLE_SPAM_FADER_SETTING),
  3835.         ));
  3836.       }
  3837.    
  3838.       /**
  3839.        * Get post stats for post
  3840.        * @param {Element} post div.post
  3841.        * @return {PosterStats}
  3842.        */
  3843.       static getPosterStats(post) {
  3844.         const posterId = EightKun.getPosterId(post);
  3845.         if (!SpamFader.posterStats.has(posterId)) {
  3846.           SpamFader.posterStats.set(posterId, new PosterStats(posterId));
  3847.         }
  3848.         return SpamFader.posterStats.get(posterId);
  3849.       }
  3850.    
  3851.       /**
  3852.        * Adds spam badge to the posts by the poster.
  3853.        * Wear them proudly fag!
  3854.        *
  3855.        * @param {PosterStats} posterStats The posterStats object representing
  3856.        *        spam fag
  3857.        * @param {string} badge Font-Awesome glyph for badge
  3858.        * @param {string} badgeTitle The title describing the badge
  3859.        */
  3860.       static addSpamBadge(posterStats, badge, badgeTitle) {
  3861.         posterStats.posts.forEach(function(post) {
  3862.           if (!$(post).find(SpamFader.SPAM_BADGES_SELECTOR).length) {
  3863.             SpamFader.createSpamBadgeSection(post);
  3864.           }
  3865.           const alreadyHasBadge = $(post)
  3866.               .find(SpamFader.SPAM_BADGES_SELECTOR)
  3867.               .find(`.fa-${badge}`).length;
  3868.    
  3869.           if (!alreadyHasBadge) {
  3870.             $(post).find(SpamFader.SPAM_BADGES_SELECTOR).append(
  3871.                 `<i class="fa fa-${badge}" title='${badgeTitle}'></i>`,
  3872.             );
  3873.           }
  3874.         });
  3875.       }
  3876.    
  3877.       /**
  3878.        * Create section for spam badges
  3879.        * @param {Element} post div.post
  3880.        */
  3881.       static createSpamBadgeSection(post) {
  3882.         const $postModifiedSection = $(post).find(EightKun.POST_MODIFIED_SELECTOR);
  3883.         const button = $(`<button class='${SpamFader.NOT_SPAM_BUTTON_CLASS}'>
  3884.             <i class="fa fa-undo" title='Not spam'></i>Not Spam
  3885.           </button>
  3886.         `);
  3887.    
  3888.         button.click(function(e) {
  3889.           e.preventDefault();
  3890.           SpamFader.markNotSpam(post);
  3891.         });
  3892.    
  3893.         button.appendTo($postModifiedSection);
  3894.    
  3895.         $postModifiedSection.append(`
  3896.               <span class='${SpamFader.SPAM_BADGES_CLASS}'>Spam Badges:</span>`);
  3897.       }
  3898.    
  3899.       /**
  3900.        * Mark poster as not spam.
  3901.        *
  3902.        * @param {Element} post div.post
  3903.        */
  3904.       static markNotSpam(post) {
  3905.         const stats = SpamFader.getPosterStats(post);
  3906.         stats.markNotSpam();
  3907.    
  3908.         stats.posts.forEach(function(p) {
  3909.           $(p).css('opacity', 1);
  3910.           $(p).off('mouseenter mouseleave');
  3911.           $(p).find(SpamFader.SPAM_BADGES_SELECTOR).remove();
  3912.           $(p).find(`.${SpamFader.NOT_SPAM_BUTTON_CLASS}`).remove();
  3913.         });
  3914.    
  3915.         SpamFader.addToNotSpamList(stats);
  3916.       }
  3917.    
  3918.       /**
  3919.        * Save not spam in localstorage
  3920.        * @param {PosterStats} posterStats
  3921.        */
  3922.       static addToNotSpamList(posterStats) {
  3923.         const threadId = EightKun.getThreadId();
  3924.         const notSpamList = SpamFader.getNotSpamList();
  3925.         if (!(threadId in notSpamList)) {
  3926.           notSpamList[threadId] = [];
  3927.         }
  3928.         if (!SpamFader.isMarkedAsNotSpam(posterStats)) {
  3929.           notSpamList[threadId].push(posterStats.posterId);
  3930.           localStorage.setItem(SpamFader.NOT_SPAM_SETTING,
  3931.               JSON.stringify(notSpamList));
  3932.         }
  3933.       }
  3934.    
  3935.       /**
  3936.        * Has this poster been marked as not spam?
  3937.        * @param {PosterStats} posterStats
  3938.        * @return {boolean} true if not spam
  3939.        */
  3940.       static isMarkedAsNotSpam(posterStats) {
  3941.         const threadId = EightKun.getThreadId();
  3942.         const notSpamList = SpamFader.getNotSpamList();
  3943.         return threadId in notSpamList &&
  3944.             notSpamList[threadId].includes(posterStats.posterId);
  3945.       }
  3946.    
  3947.       /**
  3948.        * Get not spam list from localStorage
  3949.        * @return {Array} map of thread to not spam poster ids
  3950.        */
  3951.       static getNotSpamList() {
  3952.         return JSON.parse(
  3953.             localStorage.getItem(SpamFader.NOT_SPAM_SETTING) || '{}',
  3954.         );
  3955.       }
  3956.     }
  3957.     SpamFader.posterStats = new Map();
  3958.     SpamFader.FADE = 'fade';
  3959.     SpamFader.HIDE = 'hide';
  3960.     SpamFader.SPAM_BADGES_CLASS = 'bakertools-spam-badges';
  3961.     SpamFader.SPAM_BADGES_SELECTOR = `.${SpamFader.SPAM_BADGES_CLASS}`;
  3962.     SpamFader.MIN_OPACITY = .2;
  3963.     SpamFader.ANIMATION_DURATION = 200; // milliseconds
  3964.     SpamFader.SPAM_ACTION_SETTING = 'bakertools-spamfader-action';
  3965.     SpamFader.HIDE_SPAM_BADGES_SETTING = 'bakertools-spamfader-hide-badges';
  3966.     SpamFader.DISABLE_SPAM_FADER_SETTING = 'bakertools-spamfader-disable';
  3967.     SpamFader.NOT_SPAM_SETTING = 'bakertools-spamfader-notspam';
  3968.     SpamFader.NOT_SPAM_BUTTON_CLASS = 'bakertools-spamfader-notspam';
  3969.     SpamFader.NOT_SPAM_BUTTON_SELECTOR = `.${SpamFader.NOT_SPAM_BUTTON_CLASS}`;
  3970.    
  3971.     /**
  3972.      * Holds spam stats
  3973.      */
  3974.     class PosterStats {
  3975.       /**
  3976.        * Construct poststats for post
  3977.        * @param {number} posterId id of poster
  3978.        */
  3979.       constructor(posterId) {
  3980.         this.posts = [];
  3981.         this.posterId = posterId;
  3982.         this.markNotSpam();
  3983.       }
  3984.    
  3985.       /**
  3986.        * Reset spam indicators
  3987.        */
  3988.       markNotSpam() {
  3989.         this._spamCertainty = 0;
  3990.         this._fadeProgress = 0;
  3991.         this.floodCount = 0;
  3992.         this.breadShitCount = 0;
  3993.         this.isBreadShitter = false;
  3994.       }
  3995.    
  3996.       /**
  3997.        * Add post to poster's list of post
  3998.        * @param {Element} post div.post
  3999.        */
  4000.       addPost(post) {
  4001.         if (!this.posts.includes(post)) {
  4002.           this.posts.push(post);
  4003.         }
  4004.       }
  4005.    
  4006.       /**
  4007.        * Set spam certainty property
  4008.        * @param {number} certainty
  4009.        */
  4010.       set spamCertainty(certainty) {
  4011.         if (certainty > this._spamCertainty) {
  4012.           this._spamCertainty = certainty;
  4013.         }
  4014.       }
  4015.    
  4016.       /**
  4017.        * Get spam spamCertainty
  4018.        * @return {number} 1 represents 100% certainty.
  4019.        */
  4020.       get spamCertainty() {
  4021.         return this._spamCertainty;
  4022.       }
  4023.    
  4024.       /**
  4025.        * Set fade progress property
  4026.        * @param {number} progress
  4027.        */
  4028.       set fadeProgress(progress) {
  4029.         if (progress > this._fadeProgress) {
  4030.           this._fadeProgress = progress;
  4031.         }
  4032.       }
  4033.    
  4034.       /**
  4035.        * Get spam fade progress
  4036.        * @return {number} 1 represents 100% progress.
  4037.        */
  4038.       get fadeProgress() {
  4039.         return this._fadeProgress;
  4040.       }
  4041.    
  4042.       /**
  4043.        * Number of posts by id
  4044.        * @return {number}
  4045.        */
  4046.       get postCount() {
  4047.         return this.posts.length;
  4048.       }
  4049.    
  4050.       /**
  4051.        * Is this post spam?
  4052.        * @return {boolean} true if spam
  4053.        */
  4054.       get isSpam() {
  4055.         return this._spamCertainty >= 1;
  4056.       }
  4057.     }
  4058.    
  4059.     /**
  4060.      * Base class for spamDetectionStrategies
  4061.      */
  4062.     class SpamDetectionStrategy {
  4063.       /**
  4064.        * Determine if the provided post is spam
  4065.        * @param {Element} post div.post
  4066.        * @return {boolean} true if is spam
  4067.        */
  4068.       isSpam(post) {
  4069.         return false;
  4070.       }
  4071.     }
  4072.    
  4073.     /**
  4074.      * Marks namefags as spam
  4075.      */
  4076.     class NameFagStrategy extends SpamDetectionStrategy {
  4077.       /**
  4078.        * Construct NameFagStrategy
  4079.        */
  4080.       constructor() {
  4081.         super();
  4082.         this.nameRegex = /^Anonymous( \(You\))?$/;
  4083.         this.badge = 'tag';
  4084.         this.badgeTitle = 'Namefag';
  4085.       }
  4086.    
  4087.       /**
  4088.        * Returns true if a namefag, sets spamCertainty to 100% for post
  4089.        * to begin fading
  4090.        * @param {Element} post div.post
  4091.        * @return {boolean} true if is namefag spam
  4092.        */
  4093.       isSpam(post) {
  4094.         const isNameFag = !window.bakerTools.qPostHighlighter.isQ(post) &&
  4095.         (
  4096.           !this.nameRegex.test(EightKun.getPostName(post)) ||
  4097.           EightKun.getPostTrip(post) != ''
  4098.         );
  4099.    
  4100.         if (isNameFag) {
  4101.           const stats = SpamFader.getPosterStats(post);
  4102.           stats.spamCertainty = 1;
  4103.           stats.fadeProgress = .2;
  4104.           stats.isNameFag = true;
  4105.           SpamFader.addSpamBadge(stats, this.badge, this.badgeTitle);
  4106.         }
  4107.         return isNameFag;
  4108.       }
  4109.     }
  4110.    
  4111.     /**
  4112.      * Marks floodfags with high post count as spam
  4113.      */
  4114.     class HighPostCountFagStrategy extends SpamDetectionStrategy {
  4115.       /**
  4116.        * Construct HighPostCountFagStrategy
  4117.        */
  4118.       constructor() {
  4119.         super();
  4120.         this.postCountSpamThreshold = 15;
  4121.         this.postCountHideThreshold = 25;
  4122.         this.badge = 'bullhorn';
  4123.         this.badgeTitle = 'High Post Count Fag';
  4124.       }
  4125.    
  4126.       /**
  4127.        * Returns true if the poster has posted more than the threshold
  4128.        * @param {Element} post div.post
  4129.        * @return {boolean} true if spam
  4130.        */
  4131.       isSpam(post) {
  4132.         if (EightKun.isPostFromOp(post)) {
  4133.           return;
  4134.         }
  4135.    
  4136.         const posterStats = SpamFader.getPosterStats(post);
  4137.    
  4138.         const highCountSpamCertainty =
  4139.           Math.min(1, posterStats.postCount / this.postCountSpamThreshold);
  4140.    
  4141.         posterStats.spamCertainty = highCountSpamCertainty;
  4142.    
  4143.         if (highCountSpamCertainty === 1) {
  4144.           posterStats.isHighPostCountFag = true;
  4145.           SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
  4146.         }
  4147.    
  4148.         // We already hit spam threshold
  4149.         // Either we have hit threshold count or some other strategy says its spam
  4150.         if (posterStats.isSpam) {
  4151.           // Number of posts needed past threshold to hide
  4152.           const hideCount =
  4153.             this.postCountHideThreshold - this.postCountSpamThreshold;
  4154.    
  4155.           const progressIncrement = 1/hideCount;
  4156.    
  4157.           posterStats.fadeProgress += progressIncrement;
  4158.         }
  4159.         return posterStats.isSpam;
  4160.       }
  4161.     }
  4162.    
  4163.     /**
  4164.      * Marks floodfags with quick succession posts as spam
  4165.      */
  4166.     class FloodFagStrategy extends SpamDetectionStrategy {
  4167.       /**
  4168.        * Construct flood fag strategy
  4169.        */
  4170.       constructor() {
  4171.         super();
  4172.         this.postIntervalConsideredFlooding = 60; // seconds
  4173.         this.floodCountSpamThreshold = 5;
  4174.         this.floodCountHideThreshold = 10;
  4175.         this.badge = 'tint';
  4176.         this.badgeTitle = 'Floodfag';
  4177.       }
  4178.    
  4179.       /**
  4180.        * Returns true if a spam
  4181.        * @param {Element} post div.post
  4182.        * @return {boolean} true if is spam
  4183.        */
  4184.       isSpam(post) {
  4185.         const posterStats = SpamFader.getPosterStats(post);
  4186.         if (EightKun.isPostFromOp(post) || !this.isPostFlooded(posterStats)) {
  4187.           return;
  4188.         }
  4189.         posterStats.floodCount++;
  4190.    
  4191.         const floodSpamCertainty =
  4192.           Math.min(1, posterStats.floodCount / this.floodCountSpamThreshold);
  4193.    
  4194.         posterStats.spamCertainty = floodSpamCertainty;
  4195.    
  4196.         if (floodSpamCertainty === 1) {
  4197.           posterStats.isFloodFag = true;
  4198.           SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
  4199.         }
  4200.    
  4201.         // We already hit spam threshold
  4202.         // Either we have hit threshold count or some other strategy says its spam
  4203.         if (posterStats.isSpam) {
  4204.           // Number of posts needed past threshold to hide
  4205.           const hideCount =
  4206.             this.floodCountHideThreshold - this.floodCountSpamThreshold;
  4207.    
  4208.           const progressIncrement = 1/hideCount;
  4209.    
  4210.           posterStats.fadeProgress += progressIncrement;
  4211.         }
  4212.         return posterStats.isSpam;
  4213.       }
  4214.    
  4215.       /**
  4216.        * Is this a flooded post?
  4217.        * @param {PosterStats} posterStats
  4218.        * @return {boolean} true if flooded
  4219.        */
  4220.       isPostFlooded(posterStats) {
  4221.         if (posterStats.posts.length <= 1) {
  4222.           return false;
  4223.         }
  4224.         const currentPost = posterStats.posts.slice(-1)[0];
  4225.         const previousPost = posterStats.posts.slice(-2)[0];
  4226.         const previousPostTime = EightKun.getPostTime(previousPost);
  4227.         const currentPostTime = EightKun.getPostTime(currentPost);
  4228.         return (currentPostTime - previousPostTime) <=
  4229.           this.postIntervalConsideredFlooding;
  4230.       }
  4231.     }
  4232.    
  4233.     /**
  4234.      * Marks breadshitters as spam
  4235.      */
  4236.     class BreadShitterFagStrategy extends SpamDetectionStrategy {
  4237.       // TODO: dont check for bread shitting on non research thread?
  4238.       /**
  4239.        * Construct flood fag strategy
  4240.        */
  4241.       constructor() {
  4242.         super();
  4243.         // Let's go easy, maybe its a newfag?
  4244.         this.breadShittingIncrement = .1;
  4245.         this.badge = 'clock-o';
  4246.         this.badgeTitle = 'Bread shitter';
  4247.       }
  4248.    
  4249.       /**
  4250.        * Returns true if a spam
  4251.        * @param {Element} post div.post
  4252.        * @return {boolean} true if is spam
  4253.        */
  4254.       isSpam(post) {
  4255.         const posterStats = SpamFader.getPosterStats(post);
  4256.         if (EightKun.isPostFromOp(post) || !this.isBreadShitter(post)) {
  4257.           return;
  4258.         }
  4259.         posterStats.breadShitCount++;
  4260.    
  4261.         posterStats.isBreadShitter = true;
  4262.         SpamFader.addSpamBadge(posterStats, this.badge, this.badgeTitle);
  4263.         posterStats.spamCertainty = 1;
  4264.         posterStats.fadeProgress += this.breadShittingIncrement;
  4265.    
  4266.         return posterStats.isSpam;
  4267.       }
  4268.    
  4269.       /**
  4270.        * Is this a bread shitting post?
  4271.        * @param {Element} post div.post
  4272.        * @return {boolean} true if bread shitter
  4273.        */
  4274.       isBreadShitter(post) {
  4275.         const dough = ResearchBread.getDoughPost();
  4276.         const doughTime = EightKun.getPostTime(dough);
  4277.         const postTime = EightKun.getPostTime(post);
  4278.    
  4279.         return postTime <= doughTime;
  4280.       }
  4281.     }
  4282.    
  4283.     /* exported StatsOverlay */
  4284.     /* global $, EightKun, QPostHighlighter, NotablePost, debounce */
  4285.     /**
  4286.     * Overlays bread stats (and some other controls) in the bottom right of the
  4287.     * screen.
  4288.     */
  4289.     class StatsOverlay {
  4290.       /**
  4291.        * Construct statsoverlay, html element, setup listeners
  4292.        */
  4293.       constructor() {
  4294.         this.id = 'bakertools-stats-overlay';
  4295.         this.maxPosts = 750;
  4296.         this.postCountId = 'bakertools-stats-post-count';
  4297.         this.userCountId = 'bakertools-stats-uid-count';
  4298.         this.qCountId = 'bakertools-stats-q-count';
  4299.         this.notableCountId = 'bakertools-stats-notable-count';
  4300.    
  4301.         this._createStyles();
  4302.         this._createElement();
  4303.         this._updateStats();
  4304.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  4305.           this._updateStats();
  4306.         }.bind(this));
  4307.       }
  4308.    
  4309.       /**
  4310.        * Create styles for stats overlay
  4311.        */
  4312.       _createStyles() {
  4313.         const sheet = window.document.styleSheets[0];
  4314.         sheet.insertRule(`#${this.id} {
  4315.           padding: 5px;
  4316.           position: fixed;
  4317.           z-index: 100;
  4318.           float: right;
  4319.           right:28.25px;
  4320.           bottom: 28.25px;
  4321.         }`, sheet.cssRules.length);
  4322.       }
  4323.    
  4324.       /**
  4325.        * Create actual html element for style overlay
  4326.        */
  4327.       _createElement() {
  4328.         this.element = document.createElement('div');
  4329.         this.element.id = this.id;
  4330.    
  4331.         this.$goToLast = $(`
  4332.         <a href="javascript:void(0)" alt="last" title="Go to last reading location">
  4333.         <i class="fa fa-step-backward"></i>
  4334.         </a>`);
  4335.    
  4336.    
  4337.         this.saveLastReadingLocation = debounce(this.saveLastReadingLocation, 450);
  4338.         this.currentReadingLocation = $(window).scrollTop();
  4339.         $(window).scroll(function() {
  4340.           this.saveLastReadingLocation();
  4341.         }.bind(this));
  4342.    
  4343.         this.$goToLast.click(function() {
  4344.           $(window).scrollTop(this.lastReadingLocation);
  4345.         }.bind(this));
  4346.    
  4347.         $(this.element).append( `
  4348.         Posts: <span id="${this.postCountId}" ></span>
  4349.         UIDS: <span id="${this.userCountId}"></span>
  4350.         `);
  4351.    
  4352.         $(this.element).append(this.$goToLast);
  4353.    
  4354.         $(this.element).append(`
  4355.         <a href="#bottom" alt="to-bottom" title="Go to bottom">
  4356.         <i class="fa fa-angle-double-down"></i>
  4357.         </a>
  4358.         <a href="#top" alt="to-top" title="Go to top">
  4359.         <i class="fa fa-angle-double-up"></i>
  4360.         </a>
  4361.    
  4362.         <br/>
  4363.    
  4364.         Q's: <span id="${this.qCountId}" ></span>
  4365.        Notables: <span id="${this.notableCountId}"></span>
  4366.        `);
  4367.        document.body.appendChild(this.element);
  4368.        this._setPostCount($('div.post.reply').length);
  4369.      }
  4370.    
  4371.      /**
  4372.       * Save the last spot before scrolling or navigation
  4373.       */
  4374.      saveLastReadingLocation() {
  4375.        const scrollDistance = Math.abs(
  4376.            this.currentReadingLocation - $(window).scrollTop());
  4377.        const scrolledMoreThanThirdScreenHeight =
  4378.            scrollDistance > (window.innerHeight / 3);
  4379.    
  4380.        if (!scrolledMoreThanThirdScreenHeight) {
  4381.          return;
  4382.        }
  4383.        this.lastReadingLocation = this.currentReadingLocation;
  4384.        this.currentReadingLocation = $(window).scrollTop();
  4385.      }
  4386.    
  4387.      /**
  4388.       * Update the stats fields
  4389.       */
  4390.      _updateStats() {
  4391.        const postCount = $('#thread_stats_posts').text();
  4392.    
  4393.        if (postCount) {
  4394.          this._setPostCount(postCount);
  4395.        }
  4396.        // TODO: uids dont load at first load.
  4397.        $('#'+this.userCountId).text($('#thread_stats_uids').text() || '0');
  4398.        $('#'+this.qCountId).text(QPostHighlighter.qPosts.length);
  4399.        $('#'+this.notableCountId).text(NotablePost.getNotables().length);
  4400.      }
  4401.    
  4402.      /**
  4403.       * Set post count in overlay
  4404.       * @param {number} count
  4405.       */
  4406.      _setPostCount(count) {
  4407.        const progress = count/this.maxPosts;
  4408.        let postColor = 'green';
  4409.        if (progress >= .87) { // ~ 650 posts (100 posts left)
  4410.          postColor = 'red';
  4411.        } else if (progress >= .5) {
  4412.          postColor = 'goldenrod';
  4413.        }
  4414.        $('#'+this.postCountId).text(count).css({'color': postColor});
  4415.      }
  4416.    } // End StatsOverlay class
  4417.    
  4418.    /* global $, NavigationControl, EightKun, ResearchBread,
  4419.     ColorPicker, POST_BACKGROUND_CHANGE_EVENT, BakerWindow */
  4420.    /**
  4421.    * Highlight posts that (you)
  4422.    * Adds (You) navigation links to baker window
  4423.    */
  4424.    class YouHighlighter {
  4425.      /**
  4426.       * Construct YN object
  4427.       */
  4428.      constructor() {
  4429.        this.styleId = 'bakertools-you-styles';
  4430.        this.yous = [];
  4431.        this.ownPosts = [];
  4432.    
  4433.        this.showYouNavigationInBoardListId =
  4434.            'bakertools-show-you-nav-in-boardlist';
  4435.    
  4436.        this.showOwnNavigationInBoardListId =
  4437.            'bakertools-show-own-nav-in-boardlist';
  4438.    
  4439.        this._createStyles();
  4440.        this._setupBakerWindowControls();
  4441.        this._readSettings();
  4442.    
  4443.        this._initOwnAndYouPosts();
  4444.        this._setupListeners();
  4445.      }
  4446.    
  4447.      /**
  4448.       * Create styles
  4449.       */
  4450.      _createStyles() {
  4451.        $('head').append(`
  4452.        <style id='${this.styleId}'>
  4453.        ${EightKun.POST_SELECTOR}.${YouHighlighter.YOU_CLASS} {
  4454.          background: ${this.youColor};
  4455.        }
  4456.    
  4457.        ${EightKun.POST_SELECTOR}.${YouHighlighter.OWN_CLASS} {
  4458.          background: ${this.ownPostColor};
  4459.        }
  4460.        </style>
  4461.        `);
  4462.      }
  4463.    
  4464.      /**
  4465.       * Read settings from localStorage
  4466.       */
  4467.      _readSettings() {
  4468.        this.showYouNavigationInBoardList(JSON.parse(
  4469.            localStorage.getItem(
  4470.                YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING),
  4471.        ));
  4472.    
  4473.        this.showOwnNavigationInBoardList(JSON.parse(
  4474.            localStorage.getItem(
  4475.                YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING),
  4476.        ));
  4477.      }
  4478.    
  4479.      /**
  4480.       * Add (you) navigation to bakerwindow
  4481.       */
  4482.      _setupBakerWindowControls() {
  4483.        const youColorPicker = new ColorPicker(
  4484.            '(You) Post Color',
  4485.            'Set background color of posts replying to (you)',
  4486.            YouHighlighter.YOU_COLOR_SETTING,
  4487.            YouHighlighter.DEFAULT_YOU_COLOR,
  4488.            (color) => this.youColor = color,
  4489.        );
  4490.    
  4491.        const ownPostColorPicker = new ColorPicker(
  4492.            'Own Post Color',
  4493.            'Set background color your own posts',
  4494.            YouHighlighter.OWN_COLOR_SETTING,
  4495.            YouHighlighter.DEFAULT_OWN_COLOR,
  4496.            (color) => this.ownPostColor = color,
  4497.        );
  4498.    
  4499.        window.bakerTools.mainWindow.addColorOption(ownPostColorPicker.element);
  4500.        window.bakerTools.mainWindow.addColorOption(youColorPicker.element);
  4501.    
  4502.        this.ownNavigation = new NavigationControl('Own Posts',
  4503.        this.getOwnPosts.bind(this), YouHighlighter.NEW_OWN_POST_EVENT);
  4504.    
  4505.        this.ownBoardListNav = new NavigationControl('Own',
  4506.            this.getOwnPosts.bind(this), YouHighlighter.NEW_OWN_POST_EVENT);
  4507.    
  4508.        $(EightKun.getTopBoardlist()).append(this.ownBoardListNav.element);
  4509.        $(this.ownBoardListNav.element).hide();
  4510.    
  4511.        window.bakerTools.mainWindow.addNavigationOption(`
  4512.        <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  4513.          <label for="${this.showOwnNavigationInBoardListId}"
  4514.            title="Show navigation controls in board list bar" >
  4515.            Show Own Post Nav in Board List:
  4516.          </label>
  4517.          <input type="checkbox" id="${this.showOwnNavigationInBoardListId}"
  4518.            title="Show navigation controls in board list bar" /><br />
  4519.        </div>
  4520.        `);
  4521.    
  4522.        window.bakerTools.mainWindow.addNavigation(this.ownNavigation.element);
  4523.    
  4524.        this.youNavigation = new NavigationControl(`(You)'s`,
  4525.             this.getYous.bind(this), YouHighlighter.NEW_YOU_POST_EVENT);
  4526.    
  4527.         this.youBoardListNav = new NavigationControl('You',
  4528.             this.getYous.bind(this), YouHighlighter.NEW_YOU_POST_EVENT);
  4529.    
  4530.         $(EightKun.getTopBoardlist()).append(this.youBoardListNav.element);
  4531.         $(this.youBoardListNav.element).hide();
  4532.    
  4533.         window.bakerTools.mainWindow.addNavigationOption(`
  4534.         <div class='${BakerWindow.CONTROL_GROUP_CLASS}'>
  4535.           <label for="${this.showYouNavigationInBoardListId}"
  4536.             title="Show navigation controls in board list bar" >
  4537.             Show (You) Nav in Board List:
  4538.           </label>
  4539.           <input type="checkbox" id="${this.showYouNavigationInBoardListId}"
  4540.             title="Show navigation controls in board list bar" /><br />
  4541.         </div>
  4542.         `);
  4543.    
  4544.    
  4545.         window.bakerTools.mainWindow.addNavigation(this.youNavigation.element);
  4546.       }
  4547.    
  4548.       /**
  4549.        * Set the background color of posts replying to (you)
  4550.        * @param {string} color A valid css color value.
  4551.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  4552.        */
  4553.       set youColor(color) {
  4554.         this._youColor = color;
  4555.         document.getElementById(this.styleId)
  4556.             .sheet.cssRules[0].style.background = color;
  4557.         $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  4558.       }
  4559.    
  4560.       /**
  4561.        * Get background color for posts replying to (you)
  4562.        */
  4563.       get youColor() {
  4564.         return this._youColor;
  4565.       }
  4566.    
  4567.       /**
  4568.        * Set the background color of your own posts
  4569.        * @param {string} color A valid css color value.
  4570.        *        E.G. ('#ff00ee', 'rgba()' or 'blue')
  4571.        */
  4572.       set ownPostColor(color) {
  4573.         this._ownPostColor = color;
  4574.         document.getElementById(this.styleId)
  4575.             .sheet.cssRules[1].style.background = color;
  4576.         $(document).trigger(POST_BACKGROUND_CHANGE_EVENT, color);
  4577.       }
  4578.    
  4579.       /**
  4580.        * Get background color for your own posts
  4581.        */
  4582.       get ownPostColor() {
  4583.         return this._ownPostColor;
  4584.       }
  4585.    
  4586.       /**
  4587.        * Setup listeners for baker window controls
  4588.        */
  4589.       _setupListeners() {
  4590.         $('#'+this.showOwnNavigationInBoardListId).change(function(e) {
  4591.           this.showOwnNavigationInBoardList(e.target.checked);
  4592.         }.bind(this));
  4593.    
  4594.         $('#'+this.showYouNavigationInBoardListId).change(function(e) {
  4595.           this.showYouNavigationInBoardList(e.target.checked);
  4596.         }.bind(this));
  4597.    
  4598.         $(document).on(EightKun.NEW_POST_EVENT, function(e, post) {
  4599.           if (this.isAYou(post)) {
  4600.             this._addYouPost(post);
  4601.           }
  4602.           if (this.isOwnPost(post)) {
  4603.             this._addOwnPost(post);
  4604.           }
  4605.         }.bind(this));
  4606.       }
  4607.    
  4608.       /**
  4609.        * Show/hide you nav in boardlist
  4610.        *
  4611.        * @param {boolean} showYouNavInBoardList
  4612.        */
  4613.       showYouNavigationInBoardList(showYouNavInBoardList) {
  4614.         $('#'+this.showYouNavigationInBoardListId).prop('checked',
  4615.             showYouNavInBoardList);
  4616.    
  4617.         localStorage.setItem(YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING,
  4618.             showYouNavInBoardList);
  4619.    
  4620.         if (showYouNavInBoardList) {
  4621.           $(this.youBoardListNav.element).show();
  4622.         } else {
  4623.           $(this.youBoardListNav.element).hide();
  4624.         }
  4625.       }
  4626.    
  4627.       /**
  4628.        * Show/hide own nav in boardlist
  4629.        *
  4630.        * @param {boolean} showOwnNavInBoardList
  4631.        */
  4632.       showOwnNavigationInBoardList(showOwnNavInBoardList) {
  4633.         $('#'+this.showOwnNavigationInBoardListId).prop('checked',
  4634.             showOwnNavInBoardList);
  4635.    
  4636.         localStorage.setItem(YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING,
  4637.             showOwnNavInBoardList);
  4638.    
  4639.         if (showOwnNavInBoardList) {
  4640.           $(this.ownBoardListNav.element).show();
  4641.         } else {
  4642.           $(this.ownBoardListNav.element).hide();
  4643.         }
  4644.       }
  4645.    
  4646.       /**
  4647.        * Get (You)'s
  4648.        * @return {Array} of div.post
  4649.        */
  4650.       getYous() {
  4651.         return this.yous;
  4652.       }
  4653.    
  4654.       /**
  4655.        * Is the post replying to you
  4656.        * @param {Element} post div.post
  4657.        * @return {boolean} True if post is replying to you
  4658.        */
  4659.       isAYou(post) {
  4660.         return post.querySelector('.body')
  4661.             .innerHTML
  4662.             .indexOf('<small>(You)</small>') != -1;
  4663.       }
  4664.    
  4665.       /**
  4666.        * Is this your own post
  4667.        * @param {Element} post div.post
  4668.        * @return {boolean} True if post is you
  4669.        */
  4670.       isOwnPost(post) {
  4671.         return $(post).hasClass('you');
  4672.       }
  4673.    
  4674.       /**
  4675.        * Add you post and trigger event
  4676.        * @param {Element} post
  4677.        */
  4678.       _addYouPost(post) {
  4679.         this.yous.push(post);
  4680.         $(post).addClass(YouHighlighter.YOU_CLASS);
  4681.         $(document).trigger(YouHighlighter.NEW_YOU_POST_EVENT, post);
  4682.       }
  4683.    
  4684.       /**
  4685.        * Add own post and trigger event
  4686.        * @param {Element} post
  4687.        */
  4688.       _addOwnPost(post) {
  4689.         this.ownPosts.push(post);
  4690.         $(post).addClass(YouHighlighter.OWN_CLASS);
  4691.         $(document).trigger(YouHighlighter.NEW_OWN_POST_EVENT, post);
  4692.       }
  4693.    
  4694.       /**
  4695.        * Get own and you posts that are present at page load
  4696.        */
  4697.       _initOwnAndYouPosts() {
  4698.         const ownPosts = JSON.parse(localStorage.own_posts || '{}');
  4699.         const board = ResearchBread.BOARD_NAME;
  4700.    
  4701.         $('div.post').each(function(i, post) {
  4702.           const postId = $(post).attr('id').split('_')[1];
  4703.           if (ownPosts[board] &&
  4704.               ownPosts[board].indexOf(postId) !== -1) {
  4705.             this._addOwnPost(post);
  4706.           }
  4707.    
  4708.           EightKun.getReplyLinksFromPost(post).each(function(i, link) {
  4709.             const youPostId = EightKun.getPostNumberFromReplyLink(link);
  4710.    
  4711.             if (ownPosts[board] && ownPosts[board].indexOf(youPostId) !== -1) {
  4712.               this._addYouPost(post);
  4713.             }
  4714.           }.bind(this));
  4715.         }.bind(this));
  4716.    
  4717.         window.bakerTools.scrollBar.addPosts(this.ownPosts);
  4718.         window.bakerTools.scrollBar.addPosts(this.yous);
  4719.       }
  4720.    
  4721.       /**
  4722.        * Get own posts
  4723.        * @return {Array} of div.post
  4724.        */
  4725.       getOwnPosts() {
  4726.         return this.ownPosts;
  4727.       }
  4728.     }
  4729.     YouHighlighter.SHOW_YOU_NAV_IN_BOARDLIST_SETTING =
  4730.         'bakertools-show-you-nav-in-boardlist';
  4731.     YouHighlighter.SHOW_OWN_NAV_IN_BOARDLIST_SETTING =
  4732.         'bakertools-show-own-nav-in-boardlist';
  4733.     YouHighlighter.NEW_YOU_POST_EVENT =
  4734.         'bakertools-new-you-post-event';
  4735.     YouHighlighter.NEW_OWN_POST_EVENT =
  4736.         'bakertools-new-own-post-event';
  4737.     YouHighlighter.YOU_CLASS = 'bakertools-you-post';
  4738.     YouHighlighter.OWN_CLASS = 'bakertools-own-post';
  4739.     YouHighlighter.OWN_COLOR_SETTING =
  4740.         'bakertools-own-post-color';
  4741.     YouHighlighter.YOU_COLOR_SETTING =
  4742.         'bakertools-you-post-color';
  4743.     YouHighlighter.DEFAULT_OWN_COLOR = '#F8D2D2';
  4744.     YouHighlighter.DEFAULT_YOU_COLOR = '#E1B3DA';
  4745.    
  4746.    
  4747.     /* global ActivePage, $, QPostHighlighter, YouHighlighter, StatsOverlay,
  4748.      NotableHighlighter, BakerWindow, BlurImages, PreviousBreadHighlighter,
  4749.      NominatePostButtons, BreadList, ScrollbarNavigation, NotablePost,
  4750.      ImageBlacklist, PostRateChart, SpamFader */
  4751.     /**
  4752.     * MAIN
  4753.     */
  4754.     if (ActivePage.isThread()) { // Only setup the tools if we are on a thread
  4755.       $(document).ready(function() {
  4756.         console.info('Thanks for using bakertools! For God and Country! WWG1WGA');
  4757.         window.bakerTools = {};
  4758.         window.bakerTools.mainWindow = new BakerWindow();
  4759.         window.bakerTools.scrollBar = new ScrollbarNavigation([
  4760.           NotablePost.NEW_NOTABLE_POST_EVENT,
  4761.           YouHighlighter.NEW_OWN_POST_EVENT,
  4762.           YouHighlighter.NEW_YOU_POST_EVENT,
  4763.           QPostHighlighter.NEW_Q_POST_EVENT,
  4764.         ]);
  4765.         new BlurImages();
  4766.         window.bakerTools.PreviousBreadHighlighter =
  4767.           new PreviousBreadHighlighter();
  4768.         window.bakerTools.qPostHighlighter = new QPostHighlighter();
  4769.         window.bakerTools.notableHighlighter = new NotableHighlighter();
  4770.         window.bakerTools.youHighlighter = new YouHighlighter();
  4771.         window.bakerTools.statsOverlay = new StatsOverlay();
  4772.         new NominatePostButtons();
  4773.         new BreadList();
  4774.         new ImageBlacklist();
  4775.         new PostRateChart();
  4776.         new SpamFader([new NameFagStrategy(), new HighPostCountFagStrategy(),
  4777.           new FloodFagStrategy(), new BreadShitterFagStrategy()]);
  4778.       });
  4779.     }
  4780.     }(window.jQuery));
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement