Cleanreads

Cleanreads userscript for Goodreads.com

  1. // ==UserScript==
  2. // @name Cleanreads
  3. // @namespace http://hermanfassett.me
  4. // @version 1.4
  5. // @description Cleanreads userscript for Goodreads.com
  6. // @author Herman Fassett
  7. // @match https://www.goodreads.com/*
  8. // @grant GM_addStyle
  9. // ==/UserScript==
  10.  
  11. GM_addStyle( `
  12. .contentComment { padding: 10px 5px 10px 5px; }
  13. .contentClean { color: green; }
  14. .contentNotClean { color: red; }
  15. .contentUnknown { color: blue; }
  16. #crSettingsDialog {
  17. width: 500px;
  18. height: 500px;
  19. position: fixed;
  20. top: 50%;
  21. left: 50%;
  22. transform: translate(-50%, -50%);
  23. background: white;
  24. border: 1px solid rgba(0,0,0,0.15);
  25. display: none;
  26. }
  27. #crSettingsHeader {
  28. height: 50px;
  29. width: 100%;
  30. background: #F4F1EA;
  31. text-align: center;
  32. box-shadow: 0 1px 2px rgba(0,0,0,0.15);
  33. }
  34. #crSettingsHeader h1 {
  35. line-height: 50px;
  36. color: #382110;
  37. }
  38. #crSettingsHeader h1, .crSettingsHeader {
  39. font-family: "Lato", "Helvetica Neue", "Helvetica", sans-serif;
  40. }
  41. .crSettingsHeader, #crSettingsTermButtons { padding-top: 20px; }
  42. #crSettingsTermButtons button { margin-right: 5px; }
  43. #crSettingsBody { height: 400px; overflow: auto; }
  44. #crSettingsFooter {
  45. height: 50px;
  46. width: 100%;
  47. box-shadow: 1px 0 2px rgba(0,0,0,0.15);
  48. }
  49. #crSettingsFooter button {
  50. float: right;
  51. margin: 10px 10px 0 0;
  52. }
  53. #crSettingsFooter button.saveButton {
  54. color: white;
  55. background-color: #409D69;
  56. }
  57. .crTermsContainer { display: inline-block; }
  58. #crSnippetHeader {
  59. float: left;
  60. padding-right: 10px;
  61. }
  62. `);
  63.  
  64. (function(Cleanreads) {
  65. 'use strict';
  66.  
  67. /** The group bookshelf ID to use as default clean check list */
  68. Cleanreads.CLEAN_READS_BOOKSHELF_ID = 5989;
  69.  
  70. /** The positive search terms when determining verdict */
  71. Cleanreads.POSITIVE_SEARCH_TERMS = [
  72. { term: 'clean', exclude: { before: ['not', 'isn\'t'], after: ['ing'] }},
  73. { term: 'no sex', exclude: { before: [], after: [] }}
  74. ];
  75.  
  76. /** The negative search terms when determining verdict */
  77. Cleanreads.NEGATIVE_SEARCH_TERMS = [
  78. { term: 'sex', exclude: { before: ['no'], after: ['ist'] }},
  79. { term: 'adult', exclude: { before: ['young', 'new'], after: ['hood', 'ing']}},
  80. { term: 'erotic', exclude: { before: ['not', 'isn\'t'], after: []}}
  81. ];
  82.  
  83. Cleanreads.SNIPPET_HALF_LENGTH = 65;
  84. Cleanreads.ATTEMPTS = 10;
  85.  
  86. /**
  87. * Load the settings from local storage if existant
  88. */
  89. Cleanreads.loadSettings = function() {
  90. try {
  91. Cleanreads.POSITIVE_SEARCH_TERMS = JSON.parse(localStorage.getItem("Cleanreads.POSITIVE_SEARCH_TERMS")) || Cleanreads.POSITIVE_SEARCH_TERMS;
  92. Cleanreads.NEGATIVE_SEARCH_TERMS = JSON.parse(localStorage.getItem("Cleanreads.NEGATIVE_SEARCH_TERMS")) || Cleanreads.NEGATIVE_SEARCH_TERMS;
  93. Cleanreads.SNIPPET_HALF_LENGTH = JSON.parse(localStorage.getItem("Cleanreads.SNIPPET_HALF_LENGTH")) || Cleanreads.SNIPPET_HALF_LENGTH;
  94. Cleanreads.ATTEMPTS = JSON.parse(localStorage.getItem("Cleanreads.ATTEMPTS")) || Cleanreads.ATTEMPTS;
  95. Cleanreads.CLEAN_READS_BOOKSHELF = JSON.parse(localStorage.getItem("Cleanreads.CLEAN_READS_BOOKSHELF")) || {
  96. books: [],
  97. timestamp: new Date(0),
  98. unloaded: true
  99. };
  100.  
  101. // Get Clean Reads shelf clean books if not recently loaded (1 day)
  102. let now = new Date();
  103. if (now.setDate(now.getDate() - 1) > new Date(Cleanreads.CLEAN_READS_BOOKSHELF.timestamp)) {
  104. Cleanreads.getGroupBookshelfBooks(Cleanreads.CLEAN_READS_BOOKSHELF_ID, 5000)
  105. .then(data=> {
  106. Cleanreads.CLEAN_READS_BOOKSHELF = {
  107. books: data,
  108. timestamp: new Date()
  109. };
  110. localStorage.setItem("Cleanreads.CLEAN_READS_BOOKSHELF", JSON.stringify(Cleanreads.CLEAN_READS_BOOKSHELF));
  111. })
  112. .finally(Cleanreads.searchBookshelf);
  113. }
  114.  
  115. let settingsBody = document.getElementById("crSettingsBody");
  116. if (settingsBody) {
  117. settingsBody.innerHTML = `
  118. <div class="userInfoBoxContent">
  119. <div id="crSettingsTermButtons">
  120. </div>
  121. <h1 class="crSettingsHeader">Positive Search Terms:</h1>
  122. <div id="crPositiveSearchTerms">
  123. </div>
  124. <h1 class="crSettingsHeader">Negative Search Terms:</h1>
  125. <div id="crNegativeSearchTerms">
  126. </div>
  127. <h1 class="crSettingsHeader">Other Settings:</h1>
  128. <h4 id="crSnippetHeader">Snippet length:</h4> <input id="crSnippetHalfLength" type="number" value="${Cleanreads.SNIPPET_HALF_LENGTH}" min="0" />
  129. <h4 id="crAttemptsHeader">Max Verdict Load Attempts (tries every second):</h4> <input id="crAttempts" type="number" value="${Cleanreads.ATTEMPTS}" min="1" />
  130. </div>
  131. `;
  132.  
  133. // Add buttons
  134. let addPositiveButton = document.createElement("button");
  135. addPositiveButton.innerText = "Add Positive";
  136. addPositiveButton.className = "gr-button";
  137. addPositiveButton.onclick = Cleanreads.addSearchTerm.bind(null, true, null, null, null);
  138. document.getElementById("crSettingsTermButtons").appendChild(addPositiveButton);
  139. let addNegativeButton = document.createElement("button");
  140. addNegativeButton.innerText = "Add Negative";
  141. addNegativeButton.className = "gr-button";
  142. addNegativeButton.onclick = Cleanreads.addSearchTerm.bind(null, false, null, null, null);
  143. document.getElementById("crSettingsTermButtons").appendChild(addNegativeButton);
  144. let resetButton = document.createElement("button");
  145. resetButton.innerText = "Reset";
  146. resetButton.className = "gr-button";
  147. resetButton.onclick = function() {
  148. if (confirm("Are you sure you want to remove? You will have to refresh the page to see default values loaded.")) {
  149. localStorage.removeItem("Cleanreads.POSITIVE_SEARCH_TERMS");
  150. localStorage.removeItem("Cleanreads.NEGATIVE_SEARCH_TERMS");
  151. localStorage.removeItem("Cleanreads.SNIPPET_HALF_LENGTH");
  152. localStorage.removeItem("Cleanreads.ATTEMPTS");
  153. Cleanreads.loadSettings();
  154. }
  155. }
  156. document.getElementById("crSettingsTermButtons").appendChild(resetButton);
  157.  
  158. // Add existing terms
  159. Cleanreads.POSITIVE_SEARCH_TERMS.forEach((search) => Cleanreads.addSearchTerm(true, search.term, search.exclude.before, search.exclude.after));
  160. Cleanreads.NEGATIVE_SEARCH_TERMS.forEach((search) => Cleanreads.addSearchTerm(false, search.term, search.exclude.before, search.exclude.after));
  161. }
  162. } catch (ex) {
  163. console.error("Cleanreads: Failed to load settings!", ex);
  164. }
  165. };
  166.  
  167. /**
  168. * Save the positive and negative search terms to local storage
  169. */
  170. Cleanreads.saveSettings = function() {
  171. let positiveTerms = document.querySelectorAll("#crPositiveSearchTerms > .crTermsContainer");
  172. let negativeTerms = document.querySelectorAll("#crNegativeSearchTerms > .crTermsContainer");
  173.  
  174. Cleanreads.POSITIVE_SEARCH_TERMS = [...positiveTerms].map((search) => {
  175. return {
  176. term: search.querySelector("[name=term]").value,
  177. exclude: {
  178. before: search.querySelector("[name=excludeBefore]").value.split(",").map(x => x.trim()),
  179. after: search.querySelector("[name=excludeAfter]").value.split(",").map(x => x.trim())
  180. }
  181. }
  182. }).filter(x => x.term);
  183.  
  184. Cleanreads.NEGATIVE_SEARCH_TERMS = [...negativeTerms].map((search) => {
  185. return {
  186. term: search.querySelector("[name=term]").value,
  187. exclude: {
  188. before: search.querySelector("[name=excludeBefore]").value.split(",").map(x => x.trim()),
  189. after: search.querySelector("[name=excludeAfter]").value.split(",").map(x => x.trim())
  190. }
  191. }
  192. }).filter(x => x.term);
  193.  
  194. Cleanreads.SNIPPET_HALF_LENGTH = parseInt(document.getElementById("crSnippetHalfLength").value) || Cleanreads.SNIPPET_HALF_LENGTH;
  195. Cleanreads.ATTEMPTS = parseInt(document.getElementById("crAttempts").value) || Cleanreads.ATTEMPTS;
  196.  
  197. localStorage.setItem("Cleanreads.POSITIVE_SEARCH_TERMS", JSON.stringify(Cleanreads.POSITIVE_SEARCH_TERMS));
  198. localStorage.setItem("Cleanreads.NEGATIVE_SEARCH_TERMS", JSON.stringify(Cleanreads.NEGATIVE_SEARCH_TERMS));
  199. localStorage.setItem("Cleanreads.SNIPPET_HALF_LENGTH", JSON.stringify(Cleanreads.SNIPPET_HALF_LENGTH));
  200. localStorage.setItem("Cleanreads.ATTEMPTS", JSON.stringify(Cleanreads.ATTEMPTS));
  201. Cleanreads.loadSettings();
  202. }
  203.  
  204. /**
  205. * Setup the settings modal for Cleanreads
  206. */
  207. Cleanreads.setupSettings = function() {
  208. // Add link to menu dropdown
  209. let links = Array.from(document.getElementsByClassName('menuLink')).filter(x => x.innerText == 'Account settings');
  210. if (links && links.length) {
  211. let li = document.createElement('li');
  212. li.className = 'menuLink';
  213. li.onclick = Cleanreads.showSettings;
  214. li.innerHTML = `<a href='#' class='siteHeader__subNavLink'>Cleanreads settings</a>`;
  215. links[0].parentNode.insertBefore(li, links[0].nextSibling);
  216. }
  217. // Add dialog
  218. document.body.innerHTML += `
  219. <div id="crSettingsDialog">
  220. <div id="crSettingsHeader"><h1>Cleanreads Settings</h1></div>
  221. <div id="crSettingsBody">
  222. </div>
  223. <div id="crSettingsFooter"></div>
  224. </div>
  225. `;
  226. // Add link to profile page
  227. let settingsLink = document.createElement('a');
  228. settingsLink.href = '#';
  229. settingsLink.innerText = 'Cleanreads settings';
  230. settingsLink.onclick = Cleanreads.showSettings;
  231. document.getElementsByClassName('userInfoBoxContent')[0].appendChild(settingsLink);
  232. // Add close button to dialog
  233. let closeButton = document.createElement('button');
  234. closeButton.innerText = 'Close';
  235. closeButton.className = 'gr-button';
  236. closeButton.onclick = Cleanreads.hideSettings;
  237. document.getElementById('crSettingsFooter').appendChild(closeButton);
  238. // Add save button to dialog
  239. let saveButton = document.createElement('button');
  240. saveButton.innerText = 'Save';
  241. saveButton.className = 'gr-button saveButton';
  242. saveButton.onclick = Cleanreads.saveSettings;
  243. document.getElementById('crSettingsFooter').appendChild(saveButton);
  244. Cleanreads.loadSettings();
  245. };
  246.  
  247. /**
  248. * Add a search term to the settings UI
  249. */
  250. Cleanreads.addSearchTerm = function(positive, term, before, after) {
  251. document.getElementById(`cr${positive ? 'Positive' : 'Negative'}SearchTerms`).insertAdjacentHTML("beforeend",
  252. `<div class="crTermsContainer">
  253. <input name="excludeBefore" value="${before ? before.join(", ") : ''}" type="text" />
  254. <input name="term" value="${term || ''}" type="text" />
  255. <input name="excludeAfter" value="${after ? after.join(", ") : ''}" type="text" />
  256. </div>`);
  257. };
  258.  
  259. /**
  260. * Setup the rating (verdict) container on a book page
  261. */
  262. Cleanreads.setupRating = function() {
  263. let match = window.location.pathname.match(/book\/show\/(\d+)/);
  264. if (match && match.length > 1) {
  265. Cleanreads.bookId = window.location.pathname.match(/show\/(\d*)/)[1];
  266. Cleanreads.loadSettings();
  267. Cleanreads.reviews = [];
  268. Cleanreads.shelves = [];
  269. Cleanreads.positives = 0;
  270. Cleanreads.negatives = 0;
  271.  
  272. // Create container for rating
  273. let container = document.getElementById('descriptionContainer');
  274. let contentDescription = document.createElement('div');
  275. contentDescription.id = 'contentDescription';
  276. contentDescription.className = 'readable stacked u-bottomGrayBorder u-marginTopXSmall u-paddingBottomXSmall';
  277. contentDescription.innerHTML = `
  278. <h2 class="buyButtonContainer__title u-inlineBlock">Cleanreads Rating</h2>
  279. <h2 class="buyButtonContainer__title">
  280. Verdict: <span id="crVerdict">Loading...</span>
  281. (<span id="crPositives" class="contentClean">0</span>/<span id="crNegatives" class="contentNotClean">0</span>)
  282. </h2>
  283. <a id='expandCrDetails' href="#">(Details)</a>
  284. <div id="crDetails" style="display:none"></div>
  285. `;
  286. container.parentNode.insertBefore(contentDescription, container.nextSibling);
  287. Cleanreads.crDetails = document.getElementById('crDetails');
  288. document.getElementById('expandCrDetails').onclick = Cleanreads.expandDetails;
  289.  
  290. Cleanreads.getTopBookShelves(Cleanreads.bookId).then(shelves => {
  291. Cleanreads.shelves = shelves;
  292. Cleanreads.startReviews();
  293. }).catch(err => Cleanreads.startReviews());
  294. }
  295. };
  296.  
  297. /**
  298. * Start attempting to get the available reviews on the page and read their content
  299. */
  300. Cleanreads.startReviews = function() {
  301. Cleanreads.getReviews();
  302. // Reviews are delayed content so keep looking for a bit if nothing
  303. if (!Cleanreads.reviews.length && Cleanreads.ATTEMPTS--) {
  304. setTimeout(Cleanreads.startReviews, 1000);
  305. } else {
  306. Cleanreads.calculateContent();
  307. }
  308. };
  309.  
  310. /**
  311. * Get reviews from page (only gets the first page of reviews, not easy to access others without API)
  312. */
  313. Cleanreads.getReviews = function() {
  314. let reviewElements = document.querySelectorAll('#reviews .reviewText');
  315. Cleanreads.reviews = Array.from(reviewElements).map(x => (x.querySelector('[style]') || x).innerText.trim());
  316. };
  317.  
  318. /**
  319. * Get title as text with series appended
  320. */
  321. Cleanreads.getTitle = function() {
  322. return document.getElementById('bookTitle').innerText.trim() + document.getElementById('bookSeries').innerText.trim();
  323. };
  324.  
  325. /**
  326. * Get book description text
  327. */
  328. Cleanreads.getDescription = function() {
  329. let description = document.getElementById('description');
  330. return (description.querySelector('[style]') || description).innerText.trim();
  331. };
  332.  
  333. /**
  334. * Get group bookshelf titles
  335. * @param {string} shelfId - The bookshelf id
  336. * @param {number} maxCount - The maximum number of books in the bookshelf to return
  337. * @returns {Promise} - A promise that resolves to array of book ids or rejects with error
  338. */
  339. Cleanreads.getGroupBookshelfBooks = function(shelfId, maxCount) {
  340. return new Promise(function(resolve, reject) {
  341. jQuery.ajax(`${window.location.origin}/group/bookshelf/${shelfId}?utf8=✓&view=covers&per_page=${maxCount || 1000}`)
  342. .done(result => {
  343. resolve(jQuery(result).find(".rightContainer div > a").toArray().map(x => (x.href.match(/show\/(\d*)/)||[])[1]));
  344. })
  345. .fail(err => reject(err));
  346. });
  347. };
  348.  
  349. /**
  350. * Get the top 100 shelf names for a given book
  351. * @param {string} bookId - The book id
  352. * @returns {Promise} - A promise that resolves to array of top 100 shelves for given book
  353. */
  354. Cleanreads.getTopBookShelves = function(bookId) {
  355. return new Promise(function(resolve, reject) {
  356. jQuery.ajax(`${window.location.origin}/book/shelves/${bookId}`)
  357. .done(result => {
  358. resolve(jQuery(result).find('.shelfStat').toArray().map(x => `${jQuery(x).find('.actionLinkLite').text().replace(/-/gi, ' ')} (${jQuery(x).find('.smallText').text().trim()})`));
  359. })
  360. .fail(err => reject(err));
  361. });
  362. };
  363.  
  364. /**
  365. * Get list titles
  366. * TODO: currently only gets first page
  367. * @param {string} listId - The list id
  368. * @returns {Promise} - A promise that resolves to array of book ids or rejects with error
  369. */
  370. Cleanreads.getListBooks = function(listId) {
  371. return new Promise(function(resolve, reject) {
  372. jQuery.ajax(`${window.location.origin}/list/show/${listId}`)
  373. .done(result => {
  374. resolve(jQuery(result).find(".tableList tr td:nth-child(2) div:nth-child(1)").toArray().map(x => x.id))
  375. })
  376. .fail(err => {
  377. reject(err);
  378. });
  379. });
  380. };
  381.  
  382. /**
  383. * Calculate the cleanliness
  384. */
  385. Cleanreads.calculateContent = function() {
  386. let count = 0, containing = [];
  387. // Insert containers for bases
  388. Cleanreads.crDetails.innerHTML +=
  389. `<h2 class="buyButtonContainer__title u-marginTopXSmall">Bookshelf Content Basis: </h2>
  390. <div id="bookshelfBasis">
  391. <i class="contentComment">
  392. Loading
  393. <a href="${window.location.origin}/group/bookshelf/${Cleanreads.CLEAN_READS_BOOKSHELF_ID}">Clean Reads bookshelf</a>
  394. </i>
  395. </div>`;
  396. Cleanreads.crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Description Content Basis: </h2><div id="descriptionBasis"></div>`;
  397. Cleanreads.crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Clean Basis: </h2><div id="cleanBasis"></div>`;
  398. Cleanreads.crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Not Clean Basis: </h2><div id="notCleanBasis"></div>`;
  399. // Get containers
  400. let descriptionBasis = document.getElementById('descriptionBasis'),
  401. cleanBasis = document.getElementById('cleanBasis'),
  402. notCleanBasis = document.getElementById('notCleanBasis');
  403.  
  404. // Search description
  405. let description = `Title: ${Cleanreads.getTitle()}\nDescription: ${Cleanreads.getDescription()}`;
  406. Cleanreads.searchContent(Cleanreads.POSITIVE_SEARCH_TERMS, [description], descriptionBasis, true, Cleanreads.insertComment);
  407. Cleanreads.searchContent(Cleanreads.NEGATIVE_SEARCH_TERMS, [description], descriptionBasis, false, Cleanreads.insertComment);
  408.  
  409. // Search top shelves
  410. Cleanreads.searchContent(Cleanreads.POSITIVE_SEARCH_TERMS, Cleanreads.shelves, cleanBasis, true, Cleanreads.insertShelf);
  411. Cleanreads.searchContent(Cleanreads.NEGATIVE_SEARCH_TERMS, Cleanreads.shelves, notCleanBasis, false, Cleanreads.insertShelf);
  412.  
  413. // Search reviews
  414. Cleanreads.searchContent(Cleanreads.POSITIVE_SEARCH_TERMS, Cleanreads.reviews, cleanBasis, true, Cleanreads.insertComment);
  415. Cleanreads.searchContent(Cleanreads.NEGATIVE_SEARCH_TERMS, Cleanreads.reviews, notCleanBasis, false, Cleanreads.insertComment);
  416.  
  417. // Fill bases if nothing
  418. if (!descriptionBasis.innerHTML) {
  419. descriptionBasis.innerHTML = '<i class="contentComment">None</i>';
  420. }
  421. if (!cleanBasis.innerHTML) {
  422. cleanBasis.innerHTML = '<i class="contentComment">None</i>';
  423. }
  424. if (!notCleanBasis.innerHTML) {
  425. notCleanBasis.innerHTML = '<i class="contentComment">None</i>';
  426. }
  427.  
  428. // Update Clean Reads verdict
  429. if (!Cleanreads.CLEAN_READS_BOOKSHELF.unloaded) {
  430. Cleanreads.updateVerdict();
  431. Cleanreads.searchBookshelf();
  432. }
  433. };
  434.  
  435. /**
  436. * Function to search for terms in a given string
  437. * @param {term} term - Term object to match in content
  438. * @param {string} content - Content to search
  439. * @returns {Array} - RegExp result array
  440. */
  441. Cleanreads.matchTerm = function(term, content) {
  442. let regex = new RegExp(`(^|[^(${term.exclude.before.join`|`}|\\s*)])(\\W*)(${term.term})(\\W*)($|[^(${term.exclude.after.join`|`}|\\s*)])`);
  443. let contentMatch = content.toLowerCase().match(regex);
  444. return contentMatch;
  445. }
  446.  
  447. /**
  448. * Search string array for given list of terms, add matches to given container, and increment positive/negative verdict
  449. * @param {term object array} terms - Terms to search for
  450. * @param {string array} contents - Contents to search
  451. * @param {element} container - Result container
  452. * @param {boolean} positive - Flag if positive or negative search term to determine result
  453. * @param {function} insertFunction - Function to append result to container
  454. */
  455. Cleanreads.searchContent = function(terms, contents, container, positive, insertFunction) {
  456. contents.forEach(content => {
  457. terms.forEach(term => {
  458. let contentMatch = Cleanreads.matchTerm(term, content);
  459. if (!!contentMatch) {
  460. positive ? Cleanreads.positives++ : Cleanreads.negatives++;
  461. let index = contentMatch.index + contentMatch[1].length + contentMatch[2].length;
  462. insertFunction(content, contentMatch[3], index, positive, container);
  463. }
  464. });
  465. })
  466. };
  467.  
  468. /** Insert a matched comment into given container */
  469. Cleanreads.insertComment = function(content, term, index, positive, container) {
  470. container.innerHTML += `
  471. <div class="contentComment">
  472. ...${content.slice(index - Cleanreads.SNIPPET_HALF_LENGTH, index)}<b class="content${positive ? '' : 'Not'}Clean">${
  473. content.substr(index, term.length)
  474. }</b>${content.slice(index + term.length, index + Cleanreads.SNIPPET_HALF_LENGTH)}...
  475. </div>
  476. `;
  477. };
  478.  
  479. /** Insert a matched shelf into given container */
  480. Cleanreads.insertShelf = function(content, term, index, positive, container) {
  481. container.innerHTML += `
  482. <div class="contentComment">
  483. Shelved as: ${content.slice(0, index)}<b class="content${positive ? '' : 'Not'}Clean">${
  484. content.substr(index, term.length)
  485. }</b>${content.slice(index + term.length)}
  486. </div>
  487. `;
  488. };
  489.  
  490. /**
  491. * Search the loaded bookshelf book ids for current book and update verdict
  492. */
  493. Cleanreads.searchBookshelf = function() {
  494. let bookId = window.location.pathname.match(/show\/(\d*)/)[1];
  495. let bookshelfBasis = document.getElementById('bookshelfBasis');
  496. if (bookId && Cleanreads.CLEAN_READS_BOOKSHELF.books.indexOf(bookId) != -1) {
  497. bookshelfBasis.innerHTML =
  498. `<div class="contentClean">
  499. Found in
  500. <a href="${window.location.origin}/group/bookshelf/${Cleanreads.CLEAN_READS_BOOKSHELF_ID}">Clean Reads bookshelf</a>
  501. </div>`;
  502. Cleanreads.positives++;
  503. Cleanreads.updateVerdict(true);
  504. } else {
  505. bookshelfBasis.innerHTML =
  506. `<div class="contentNotClean">
  507. Not found in
  508. <a href="${window.location.origin}/group/bookshelf/${Cleanreads.CLEAN_READS_BOOKSHELF_ID}">Clean Reads bookshelf</a>
  509. </div>`;
  510. Cleanreads.updateVerdict();
  511. }
  512. };
  513.  
  514. /**
  515. * Update the verdict shown in UI on the book
  516. * @param {boolean} overrideClean - If true, always set clean, but preserve positive/negative count
  517. */
  518. Cleanreads.updateVerdict = function(overrideClean) {
  519. let verdict = document.getElementById('crVerdict');
  520. if (overrideClean || (Cleanreads.positives && Cleanreads.positives > Cleanreads.negatives)) {
  521. verdict.innerText = `${Cleanreads.negatives ? 'Probably' : 'Most likely'} clean`;
  522. verdict.className += 'contentClean';
  523. } else if (Cleanreads.negatives && Cleanreads.negatives > Cleanreads.positives) {
  524. verdict.innerText = `${Cleanreads.positives ? 'Probably' : 'Most likely'} not clean`;
  525. verdict.className += 'contentNotClean';
  526. } else {
  527. verdict.innerText = Cleanreads.positives && Cleanreads.negatives ? 'Could be clean or not clean' : 'Unknown';
  528. verdict.className += 'contentUnknown';
  529. }
  530. document.getElementById('crPositives').innerText = Cleanreads.positives;
  531. document.getElementById('crNegatives').innerText = Cleanreads.negatives;
  532. };
  533.  
  534. /**
  535. * Expand the details section of Cleanreads verdict
  536. */
  537. Cleanreads.expandDetails = function() {
  538. let collapsedText = '(Details)',
  539. expandedText = '(Hide)';
  540. if (this.innerText == collapsedText) {
  541. Cleanreads.crDetails.style.display = 'block';
  542. this.innerText = expandedText;
  543. } else if (this.innerText == expandedText) {
  544. Cleanreads.crDetails.style.display = 'none';
  545. this.innerText = collapsedText;
  546. }
  547. };
  548.  
  549. /**
  550. * Show the settings modal for Cleanreads
  551. */
  552. Cleanreads.showSettings = function() {
  553. document.getElementById("crSettingsDialog").style.display = 'block';
  554. return false;
  555. };
  556.  
  557. /**
  558. * Hide the settings modal for Cleanreads
  559. */
  560. Cleanreads.hideSettings = function() {
  561. document.getElementById("crSettingsDialog").style.display = 'none';
  562. return false;
  563. };
  564.  
  565. // Loading. If on a book load the verdict, else if on a user page load settings
  566. if (window.location.href.match("/book/")) {
  567. Cleanreads.setupRating();
  568. } else if (window.location.href.match("/user/")) {
  569. Cleanreads.setupSettings()
  570. }
  571. })(window.Cleanreads = window.Cleanreads || {});