Advertisement
Guest User

8kun Bakertools v0.7.1

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