Advertisement
qnotables

bakertools

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