The Amazon Review Tabulator - TART

Lists all of your reviews with vote and comment tallies, with updates highlighted

  1. /*===========================================================================*\
  2. | The Amazon Review Tabulator - TART |
  3. | (c) 2016-17 by Another Floyd |
  4. | From your "Public Reviews Written by You" page on Amazon, this script |
  5. | collects and tabulates vote tallies and related information, from all of |
  6. | your Amazon reviews. Click the "Tabulate" link in the "Your Profile" |
  7. | panel. Click the heart icon, for options. |
  8. \*===========================================================================*/
  9.  
  10. // ==UserScript==
  11. // @name The Amazon Review Tabulator - TART
  12. // @namespace floyd.scripts
  13. // @version 1.5.7
  14. // @author Another Floyd at Amazon.com
  15. // @description Lists all of your reviews with vote and comment tallies, with updates highlighted
  16. // @include https://*amazon.com/gp/cdp/member-reviews*
  17. // @include https://*amazon.co.uk/gp/cdp/member-reviews*
  18. // @include https://*amazon.ca/gp/cdp/member-reviews*
  19. // @include https://*amazon.com.au/gp/cdp/member-reviews*
  20. // @grant GM_getValue
  21. // @grant GM_setValue
  22. // @grant GM_xmlhttpRequest
  23. // @grant GM_log
  24. // @grant GM_openInTab
  25. // @grant GM_info
  26. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
  27. // @require https://gf.qytechs.cn/scripts/20744-sortable/code/sortable.js?version=132520
  28. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  29. // @icon data:image/png;base64,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
  30. // ==/UserScript==
  31.  
  32. // Start
  33.  
  34. (function() {
  35.  
  36. var showUpdatesOnly = false;
  37. var primaryDisplayBuffer = "";
  38. var updateDisplayBuffer = "";
  39. var oldTARTstats = [];
  40.  
  41. var userID = "";
  42. var reviewCount = 0;
  43. var reviewerRanking = ""; // no longer used, but kept to avoid error-prone mods to newTARTstats and oldTARTstats structure
  44. var helpfulVotes = 0;
  45.  
  46. var oldStoreItemIDs = [];
  47. var oldStoreUpvotes = [];
  48. var oldStoreDownvotes = [];
  49. var oldStoreComments = [];
  50.  
  51. var newStoreItemIDs = "";
  52. var newStoreUpvotes = "";
  53. var newStoreDownvotes = "";
  54. var newStoreComments = "";
  55.  
  56. var tallyWordcount = 0;
  57. var tallyUpvotes = 0;
  58. var tallyDownvotes = 0;
  59. var tallyAllvotes = 0;
  60. var tallyStars = 0;
  61. var tallyComments = 0;
  62. var tallyAVP = 0;
  63. var tallyVine = 0;
  64.  
  65. // use this reference for progress indicator
  66. var profileDiv = "";
  67. var profileDivOriginalHTML = "";
  68. var profileDivTabulateHTML = "<br></br><a href='javascript:tabulate();'>Tabulate</a> <a href='javascript:options();' title='Click for TART options' style='text-decoration:none;font-size:135%;font-weight:bold'>" + String.fromCharCode(9829) + "</a>";
  69.  
  70. function assembleDisplayBuffers (completeSetOfTableRows, reviewsProcessed) {
  71.  
  72. var today = new Date();
  73. var formattedToday = today.toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'});
  74. var toggleLink = (GM_config.get('DisplayMode')) ? "<p><a href='javascript:toggleView();'>Toggle View: All Reviews | Updates Only</a>" : "";
  75. var bMargin = (GM_config.get('FixedFooter')) ? "36" : "0";
  76. var upvoteReviewRatio = (helpfulVotes/reviewCount).toFixed(2);
  77.  
  78. // set up top of display page
  79. primaryDisplayBuffer = "<!DOCTYPE html><html lang='en'>" +
  80. "<head><meta charset='utf-8'/><title>TART Amazon Review Details</title>" +
  81. "<style type='text/css'>" +
  82. "body {font-family:Arial,sans-serif;font-size:" + GM_config.get('FontSize') + "px; margin:0; padding:0px 5px}" +
  83. ".tg {border-collapse:collapse;border-spacing:0;width:100%}" +
  84. ".tg td{padding:" + GM_config.get('RowPadding') + "px 4px; border-style:solid; border-width:1px; overflow:hidden; word-break:normal; font-size:" + GM_config.get('FontSize') + "px; text-align:right}" +
  85. ".tg th{padding:" + GM_config.get('RowPadding') + "px 4px; border-style:solid; border-width:1px; overflow:hidden; word-break:normal; font-size:" + GM_config.get('FontSize') + "px; text-align:right; font-weight:bold; background-color:#010066; color:#ffffff}" +
  86. ".tg .cell-left{text-align:left}" +
  87. ".tg .hilite-left{text-align:left;background-color:#" + GM_config.get('HighliteColor') + "}" +
  88. ".tg .hilite-right{background-color:#" + GM_config.get('HighliteColor') + "}" +
  89. "#tblMain.hide7 tr td:nth-child(7), #tblMain.hide7 tr th:nth-child(7) {display: none}" +
  90. "#tblMain.hide10 tr td:nth-child(10), #tblMain.hide10 tr th:nth-child(10) {display: none}" +
  91. "#tblMain.hide11 tr td:nth-child(11), #tblMain.hide11 tr th:nth-child(11) {display: none}" +
  92. "#footer {position:fixed; bottom:0}" +
  93. "#footer.hide7 tr td:nth-child(7), #footer.hide7 tr th:nth-child(7) {display: none}" +
  94. "#footer.hide10 tr td:nth-child(10), #footer.hide10 tr th:nth-child(10) {display: none}" +
  95. "#footer.hide11 tr td:nth-child(11), #footer.hide11 tr th:nth-child(11) {display: none}" +
  96. ".txtLarge {font-size:18px;font-weight:bold}" +
  97. ".summaryLink, .summaryLink:link, .summaryLink:visited {text-decoration:none; font-weight:bold; color:#000000}" +
  98. ".tableLink, .tableLink:link, .tableLink:visited {text-decoration:none; font-weight:bold; font-size:110%; color:#000000}" +
  99. ".footerLink, .footerLink:link, .footerLink:visited {text-decoration:none; font-weight:bold; color:#FFFF12}" +
  100. "table.sortable th.sorted {background-color:#000000}" +
  101. "</style></head><body>" +
  102. "<span class='txtLarge'>Amazon Review Details</span><br>" +
  103. "Prepared with <a href='https://gf.qytechs.cn/en/scripts/24434-the-amazon-review-tabulator-tart' target='_new'>TART " + GM_info.script.version + "</a> - " + formattedToday +
  104. "<p>Review Count: " + checkChange(reviewCount, oldTARTstats[6], false) + "<br>" +
  105. "Helpful Votes: " + checkChange(helpfulVotes, oldTARTstats[7], false) + "<br>" +
  106. "Upvote/Review Ratio: " + checkChange(upvoteReviewRatio, oldTARTstats[8], false) + toggleLink +
  107. "</p><table class='tg sortable' id='tblMain' style='margin-bottom:" + bMargin + "px'>" +
  108. "<thead><tr>" +
  109. "<th class='cell-left sort-number sort-default' style='width:6%'>#</th>" +
  110. "<th class='cell-left sort-text' style='width:33%'>Item</th>" +
  111. "<th class='cell-left sort-date' style='width:12%'>Date</th>" +
  112. "<th class='sort-number'>Stars</th>" +
  113. "<th class='sort-number'>Upvotes</th>" +
  114. "<th class='sort-number'>Downvotes</th>" +
  115. "<th class='sort-number'>All&nbsp;Votes</th>" +
  116. "<th class='sort-number'>%&nbsp;Helpful</th>" +
  117. "<th class='sort-number'>Comments</th>" +
  118. "<th class='sort-text' style='width:6%'>AVP</th>" +
  119. "<th class='sort-text' style='width:6%'>Vine</th>" +
  120. "</tr></thead><tbody>";
  121.  
  122. // Column widths above are assigned to columns that have heading shorter than
  123. // data is likely to be; widths are duplicated at separate footer table, to keep
  124. // them all in sync
  125.  
  126. updateDisplayBuffer = primaryDisplayBuffer; // both displays have same top section
  127. primaryDisplayBuffer += completeSetOfTableRows;
  128.  
  129. // info needed in footer
  130. var calcStars = (tallyStars/reviewsProcessed).toFixed(1);
  131. var calcHelpfulPct = helpfulPercent(tallyUpvotes,tallyDownvotes);
  132. var avgWordsPerReview = (tallyWordcount/reviewsProcessed).toFixed(0);
  133. var newTARTstats = calcStars + " " + tallyUpvotes + " " + tallyDownvotes + " " + calcHelpfulPct + " " + tallyComments + " " + reviewerRanking + " " + reviewCount + " " + helpfulVotes + " " + upvoteReviewRatio + " " + tallyAllvotes + " " + tallyAVP + " " + tallyVine + " " + reviewsProcessed + " " + avgWordsPerReview;
  134. GM_setValue("recentFooterValues", newTARTstats.trim()); // write 'em with new values
  135.  
  136. var visibleFooterRow = "<tr>" +
  137. "<th style='text-align:left'>" + checkChange(reviewsProcessed, oldTARTstats[12], true) + "</th>" +
  138. "<th style='text-align:left'>Average words per review: " + checkChange(avgWordsPerReview, oldTARTstats[13], true) + "</th>" +
  139. "<th></th>" +
  140. "<th>" + checkChange(calcStars, oldTARTstats[0], true) + "</th>" +
  141. "<th>" + checkChange(tallyUpvotes, oldTARTstats[1], true) + "</th>" +
  142. "<th>" + checkChange(tallyDownvotes, oldTARTstats[2], true) + "</th>" +
  143. "<th>" + checkChange(tallyAllvotes, oldTARTstats[9], true) + "</th>" +
  144. "<th>" + checkChange(calcHelpfulPct, oldTARTstats[3], true) + "</th>" +
  145. "<th>" + checkChange(tallyComments, oldTARTstats[4], true) + "</th>" +
  146. "<th>" + checkChange(tallyAVP, oldTARTstats[10], true) + "</th>" +
  147. "<th>" + checkChange(tallyVine, oldTARTstats[11], true) + "</th>" +
  148. "</tr>";
  149.  
  150. // add footer either to be fixed at bottom of screen, or normal
  151. if(GM_config.get('FixedFooter')) {
  152. var hiddenRowForColumnWidths = "<tr style='visibility:hidden'>" +
  153. "<th style='width:6%; border-style: hidden'></th>" +
  154. "<th style='width:33%; border-style: hidden'></th>" +
  155. "<th style='width:12%; border-style: hidden'></th>" +
  156. "<th style='border-style: hidden'>Stars</th>" +
  157. "<th style='border-style: hidden'>Upvotes</th>" +
  158. "<th style='border-style: hidden'>Downvotes</th>" +
  159. "<th style='border-style: hidden'>All&nbsp;Votes</th>" +
  160. "<th style='border-style: hidden'>%&nbsp;Helpful</th>" +
  161. "<th style='border-style: hidden'>Comments</th>" +
  162. "<th style='width:6%; border-style: hidden'></th>" +
  163. "<th style='width:6%; border-style: hidden'></th></tr>";
  164.  
  165. // create detached table for footer
  166. // fixed, by virtue of the styled 'footer' id
  167. primaryDisplayBuffer += "</tbody></table><table class='tg' id='footer' style='width:calc(100% - 10px)'><tfoot>" + hiddenRowForColumnWidths + visibleFooterRow + "</tfoot></table></body></html>";
  168. }
  169. else {
  170. primaryDisplayBuffer += "</tbody><tfoot>" + visibleFooterRow + "</tfoot></table></body></html>"; // normal
  171. }
  172.  
  173. // get rows containing updated reviews, only
  174. var tempDiv = document.createElement('div');
  175. tempDiv.innerHTML = primaryDisplayBuffer;
  176.  
  177. var findUpdateRows = document.evaluate("//td[@class='hilite-left']/..", tempDiv, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  178.  
  179. for(var d = 0; d < findUpdateRows.snapshotLength; d++) {
  180. updateDisplayBuffer += findUpdateRows.snapshotItem(d).outerHTML;
  181. }
  182. updateDisplayBuffer += "</tbody></table></body></html>";
  183. }
  184.  
  185. function tabulate() {
  186.  
  187. // reset global accumulators to ensure that repeated script runs
  188. // (non-enhanced mode) remain clean
  189. newStoreItemIDs = "";
  190. newStoreUpvotes = "";
  191. newStoreDownvotes = "";
  192. newStoreComments = "";
  193.  
  194. tallyWordcount = 0;
  195. tallyUpvotes = 0;
  196. tallyDownvotes = 0;
  197. tallyAllvotes = 0;
  198. tallyStars = 0;
  199. tallyComments = 0;
  200. tallyAVP = 0;
  201. tallyVine = 0;
  202.  
  203. // read in stored info from past run, for use in change detection
  204. oldStoreItemIDs = GM_getValue("recentItemIDs", "").split(" ");
  205. oldStoreUpvotes = GM_getValue("recentUpvotes", "").split(" ");
  206. oldStoreDownvotes = GM_getValue("recentDownvotes", "").split(" ");
  207. oldStoreComments = GM_getValue("recentComments", "").split(" ");
  208.  
  209. // prepare url with country domain and user ID, ready for review page number
  210. var tld = "com";
  211. var url = window.location.href;
  212. if(url.indexOf('amazon.co.uk/') > -1) tld = "co.uk";
  213. if(url.indexOf('amazon.ca/') > -1) tld = "ca";
  214. if(url.indexOf('amazon.com.au/') > -1) tld = "com.au";
  215. var urlStart = "https://www.amazon." + tld + "/gp/cdp/member-reviews/" + userID + "?ie=UTF8&display=public&page=";
  216. var urlEnd = "&sort_by=MostRecentReview";
  217.  
  218. // space and counters for incoming data
  219. var perPageResponseDiv = [];
  220. var pageSetOfTableRows = [];
  221. var pageResponseCount = 0;
  222. var reviewsProcessed = 0;
  223. var pageCount = Math.floor(reviewCount / 10) + ((reviewCount % 10 > 0) ? 1 : 0);
  224. //var pageCount = 3; // for testing
  225.  
  226. // initialize the progress indicator
  227. // sort of pre-redundant to do this here AND in the loop, but,
  228. // looks better, if there is a lag before the first response
  229. var progressHTML = "<br></br><b>" + pageCount + "</b>";
  230. profileDiv.innerHTML = profileDivOriginalHTML + progressHTML;
  231.  
  232. // download and process Amazon pages
  233. var receivedPageWithNoReviews = false;
  234. var x = 1;
  235. while (x <= pageCount) {
  236. (function(x){
  237. var urlComplete = urlStart + x + urlEnd;
  238. perPageResponseDiv[x] = document.createElement('div');
  239.  
  240. GM_xmlhttpRequest({
  241. method: "GET",
  242. url: urlComplete,
  243. onload: function(response) {
  244.  
  245. // capture incoming data
  246. perPageResponseDiv[x].innerHTML = response.responseText;
  247. pageResponseCount++;
  248.  
  249. // update the progress indicator
  250. var progressHTML = "<br></br><b>" + (pageCount - pageResponseCount) + "</b>";
  251. profileDiv.innerHTML = profileDivOriginalHTML + progressHTML;
  252.  
  253. // get parent of any reviewText DIV
  254. var findReviews = document.evaluate("//div[@class='reviewText']/..", perPageResponseDiv[x], null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); // evaluating the doc DIV made above
  255. var reviewsOnPage = findReviews.snapshotLength;
  256. if(reviewsOnPage == 0) receivedPageWithNoReviews = true;
  257.  
  258. // process each review found on current page
  259. pageSetOfTableRows[x] = ""; // initialize each member prior to concatenating
  260.  
  261. for (var j = 0; j < reviewsOnPage; j++) {
  262.  
  263. var oneReview = findReviews.snapshotItem(j);
  264. var reviewChildren = oneReview.children;
  265. var childCount = reviewChildren.length;
  266.  
  267. var commentCount = 0;
  268. var itemTitle = "No Title Available";
  269. var itemLink = "";
  270. var permaLink = "";
  271. var starRating = 0;
  272. var reviewDate = "";
  273. var upVotes = 0;
  274. var downVotes = 0;
  275. var totalVotes = 0;
  276. var itemID = "";
  277. var isAVP = 0;
  278. var isVine = 0;
  279.  
  280. // get number of comments, and permalink
  281. var tempText = reviewChildren[childCount-2].textContent;
  282. if(tempText.indexOf('Comment (') > -1 || tempText.indexOf('Comments (') > -1) {
  283. var paren1 = tempText.indexOf('(');
  284. var paren2 = tempText.indexOf(')');
  285. commentCount = tempText.substring(paren1+1,paren2);
  286. commentCount = parseInt(commentCount.replace(/,/g, '')); // remove commas
  287. }
  288. var lst = reviewChildren[childCount-2].getElementsByTagName('a');
  289. permaLink = lst[2].getAttribute("href");
  290.  
  291. // get review wordcount and add to tally
  292. tempText = reviewChildren[childCount-3].textContent;
  293. tallyWordcount += countWords(tempText);
  294.  
  295. // the data items below do not have reliable positions, due to presence
  296. // or not, of vine voice tags, verified purchase, votes, etc.
  297. // so, are done in a loop with IF checks. Must start loop just above review
  298. // text, in case the reviewer has used any of the phrases I am searching for
  299. for (var i = childCount - 4; i > -1; i--) {
  300.  
  301. var childHTML = reviewChildren[i].innerHTML;
  302.  
  303. // get item title and item link
  304. var titleClue = childHTML.indexOf('This review is from');
  305. if(titleClue > -1) {
  306. var lst = reviewChildren[i].getElementsByTagName('a');
  307. itemLink = lst[0].getAttribute("href");
  308. itemTitle = lst[0].textContent;
  309. }
  310.  
  311. // get star rating AND review date
  312. var ratingClue = childHTML.indexOf('out of 5 stars');
  313. if(ratingClue > -1) {
  314. starRating = childHTML.substring(ratingClue-4,ratingClue-1);
  315. reviewDate = reviewChildren[i].lastElementChild.textContent;
  316. var lst = reviewDate.split(" ");
  317. reviewDate = lst[0].substring(0,3) + " " + lst[1] + " " + lst[2];
  318. }
  319.  
  320. // get vote counts
  321. var childText = reviewChildren[i].textContent;
  322. var voteClue = childText.indexOf('people found the following review helpful');
  323. if(voteClue > -1) {
  324. var list = childText.trim().split(" "); // there were extra, invisible spaces!
  325. upVotes = parseInt(list[0].replace(/,/g, '')); // remove commas
  326. totalVotes = parseInt(list[2].replace(/,/g, ''));
  327. downVotes = totalVotes - upVotes;
  328. }
  329.  
  330. // check for AVP and Vine
  331. var avpClue = childHTML.indexOf('Verified Purchase');
  332. if(avpClue > -1) isAVP = 1;
  333.  
  334. var vineClue = childHTML.indexOf('Vine Customer Review');
  335. if(vineClue > -1) isVine = 1;
  336. }
  337.  
  338. // get item ID
  339. var lst = oneReview.parentNode.getElementsByTagName('a');
  340. itemID = lst[0].getAttribute("name");
  341.  
  342. // get HTML formatted table row; rows COULD be accumulated in
  343. // preOneTableRow; but, since they come in page sets that may be
  344. // received out of order, the non-enhanced view (which has no sort,
  345. // thus no default sort) would appear out of order
  346. pageSetOfTableRows[x] += prepOneTableRow((j+1+(x-1)*10),itemID,itemTitle,permaLink,reviewDate,starRating,upVotes,downVotes,commentCount,totalVotes,isAVP,isVine);
  347.  
  348. reviewsProcessed++; // more reliable than reviewCount, for calculating avg. rating
  349. }
  350.  
  351. // clear the response, to save memory --
  352. // could be critical when there are many review pages
  353. perPageResponseDiv[x].innerHTML = "";
  354.  
  355. // see if all data from multiple page loads has arrived
  356. if(pageResponseCount==pageCount) {
  357.  
  358. // assemble the sets of table rows, which will be in proper order
  359. // rather than order received
  360. var completeSetOfTableRows = "";
  361. for(var y=1; y <= pageCount; y++) {
  362. completeSetOfTableRows += pageSetOfTableRows[y];
  363. }
  364.  
  365. assembleDisplayBuffers(completeSetOfTableRows, reviewsProcessed);
  366.  
  367. // store info to be used in subsequent run, for change detection
  368. GM_setValue("recentItemIDs", newStoreItemIDs.trim());
  369. GM_setValue("recentUpvotes", newStoreUpvotes.trim());
  370. GM_setValue("recentDownvotes", newStoreDownvotes.trim());
  371. GM_setValue("recentComments", newStoreComments.trim());
  372.  
  373. // replace progress indicator with Tabulate link
  374. profileDiv.innerHTML = profileDivOriginalHTML + profileDivTabulateHTML;
  375.  
  376. // show message if any of the received pages contained NO reviews...
  377. // SOMETHING was received -- an empty, error, or 'please try again' type page
  378. if(receivedPageWithNoReviews) {
  379. alert("A page or more of reviews was not received. \n\nReview the results, anyway, because any highlighted updates will not be highlighted in the next run. \n\nAlso, any reviews missing from the results will be highlighted in the next run, as 'new' reviews.");
  380. }
  381.  
  382. // --- display the results
  383. if(!GM_config.get('DisplayMode')) GM_openInTab("data:text/html," + encodeURIComponent(primaryDisplayBuffer));
  384. else {
  385. document.body.innerHTML = primaryDisplayBuffer;
  386. manageColumns();
  387. }
  388. }
  389. }
  390. });
  391. })(x);
  392. x++;
  393. }
  394. }
  395.  
  396. function manageColumns() {
  397. if(!GM_config.get('ShowAllVotes')) {
  398. document.getElementById("tblMain").classList.toggle("hide7");
  399. if(!showUpdatesOnly) document.getElementById("footer").classList.toggle("hide7");
  400. }
  401.  
  402. if(!GM_config.get('ShowAVP')) {
  403. document.getElementById("tblMain").classList.toggle("hide10");
  404. if(!showUpdatesOnly) document.getElementById("footer").classList.toggle("hide10");
  405. }
  406.  
  407. if(!GM_config.get('ShowVine')) {
  408. document.getElementById("tblMain").classList.toggle("hide11");
  409. if(!showUpdatesOnly) document.getElementById("footer").classList.toggle("hide11");
  410. }
  411. }
  412.  
  413. function countWords(s){ // from 'neokio' on StackOverflow
  414. s = s.replace(/\n/g,' '); // newlines to space
  415. s = s.replace(/(^\s*)|(\s*$)/gi,''); // remove spaces from start + end
  416. s = s.replace(/[ ]{2,}/gi,' '); // 2 or more spaces to 1
  417. return s.split(' ').length;
  418. }
  419.  
  420. function invalidValue(oldStoredValue) {
  421. if(oldStoredValue === undefined || oldStoredValue == "?") return true;
  422. return false;
  423. }
  424.  
  425. function checkChange(newStat,oldStat,forFooter) {
  426. if(newStat == oldStat || invalidValue(oldStat) === true) return newStat;
  427. else {
  428. var linkClass = "summaryLink";
  429. if(forFooter) linkClass = "footerLink";
  430. return "<a href='javascript: void(0)' class='" + linkClass + "' title='Previous: " + oldStat + "'>" + newStat + "</a>";
  431. }
  432. }
  433.  
  434. function toggleView() {
  435. showUpdatesOnly = !showUpdatesOnly;
  436. if(showUpdatesOnly) document.body.innerHTML = updateDisplayBuffer;
  437. else document.body.innerHTML = primaryDisplayBuffer;
  438. manageColumns();
  439. }
  440.  
  441. function helpfulPercent(upVotes,downVotes) {
  442. var helpfulPercent = "";
  443. upVotes = upVotes;
  444. downVotes = downVotes;
  445. if(upVotes + downVotes > 0) helpfulPercent = (upVotes/(upVotes+downVotes)*100).toFixed(1);
  446.  
  447. return helpfulPercent;
  448. }
  449.  
  450. function prepOneTableRow (row,itemID,itemTitle,permaLink,reviewDate,starRating,upVotes,downVotes,commentCount,totalVotes,isAVP,isVine) {
  451.  
  452. // do these before mangling the values with <b> tags </b>
  453. var helpfulPct = helpfulPercent(upVotes,downVotes);
  454. itemTitle = "<a href='" + permaLink + "' target='_new'>" + itemTitle.substring(0,40) + "</a>";
  455.  
  456. // keep tallies to use in table footer
  457. tallyUpvotes += upVotes;
  458. tallyDownvotes += downVotes;
  459. tallyAllvotes += totalVotes;
  460. tallyStars += parseInt(starRating);
  461. tallyComments += commentCount;
  462. tallyAVP += isAVP;
  463. tallyVine += isVine;
  464.  
  465. // assemble storage info, to use in subsequent run, for change detection
  466. newStoreItemIDs += itemID + " ";
  467. newStoreUpvotes += upVotes + " ";
  468. newStoreDownvotes += downVotes + " ";
  469. newStoreComments += commentCount + " ";
  470.  
  471. // see if review for this item has previously been examined
  472. var matchIdx = -1;
  473. for(var i=0; i<oldStoreItemIDs.length; i++) {
  474. if(oldStoreItemIDs[i] == itemID) {
  475. // we have a match, an item that has previously been seen
  476. matchIdx = i;
  477. break;
  478. }
  479. }
  480.  
  481. var hiliteRow = false;
  482. if(matchIdx > -1) {
  483. // entry exists; see if any of the numbers have changed
  484. if(oldStoreUpvotes[matchIdx] != upVotes) {
  485. // for changed number, make it bold, and hilite row
  486. // and store previous value for display as tooltip, for mouse hover
  487. upVotes = "<a href='javascript: void(0)' class='tableLink' title='Previous: " + oldStoreUpvotes[matchIdx] + "'>" + upVotes + "</a>";
  488. hiliteRow = true;
  489. }
  490. if(oldStoreDownvotes[matchIdx] != downVotes) {
  491. downVotes = "<a href='javascript: void(0)' class='tableLink' title='Previous: " + oldStoreDownvotes[matchIdx] + "'>" + downVotes + "</a>";
  492. hiliteRow = true;
  493. }
  494. if(oldStoreComments[matchIdx] != commentCount) {
  495. commentCount = "<a href='javascript: void(0)' class='tableLink' title='Previous: " + oldStoreComments[matchIdx] + "'>" + commentCount + "</a>";
  496. hiliteRow = true;
  497. }
  498. }
  499. else {
  500. // no match, so, it's a new review; bold the title and hilite the row
  501. itemTitle = "<b>" + itemTitle + "</b>";
  502. hiliteRow = true;
  503. }
  504.  
  505. var tdLeft = "<td class='cell-left'>";
  506. var tdRight = "<td>";
  507. if(hiliteRow===true && oldStoreItemIDs[0].length > 0) {
  508. tdLeft = "<td class='hilite-left'>";
  509. tdRight = "<td class='hilite-right'>";
  510. }
  511.  
  512. var tableRow = "<tr>" + tdLeft + row + "</td>" + tdLeft + itemTitle + "</td>" + tdLeft + reviewDate + "</td>" + tdRight + starRating + "</td>" + tdRight + upVotes + "</td>" + tdRight + downVotes + "</td>" + tdRight + totalVotes + "</td>" + tdRight + helpfulPct + "</td>" + tdRight +commentCount + "</td>" + tdRight + ((isAVP > 0) ? "&bull;" : "") + "</td>" + tdRight + ((isVine > 0) ? "&bull;" : "") + "</td></tr>";
  513.  
  514. return tableRow;
  515. }
  516.  
  517. // create Options menu with GM_config
  518.  
  519. var frame = document.createElement('div');
  520. document.body.appendChild(frame);
  521. GM_config.init(
  522. {
  523. 'id': 'MyConfig', // The id used for this instance of GM_config
  524. 'title': 'TART Options', // Panel Title
  525.  
  526. 'fields': // Fields object
  527. {
  528. 'DisplayMode': // Line item
  529. {
  530. 'type': 'checkbox',
  531. 'label': 'Enhanced display (uncheck for new tab with fewer features)',
  532. 'default': true
  533. },
  534.  
  535. 'FixedFooter':
  536. {
  537. 'type': 'checkbox',
  538. 'label': 'Show fixed footer at bottom of screen',
  539. 'default': true
  540. },
  541.  
  542. 'ShowAllVotes':
  543. {
  544. 'type': 'checkbox',
  545. 'label': 'Show All Votes column',
  546. 'default': true
  547. },
  548.  
  549. 'ShowAVP':
  550. {
  551. 'type': 'checkbox',
  552. 'label': 'Show AVP column',
  553. 'default': true
  554. },
  555.  
  556. 'ShowVine':
  557. {
  558. 'type': 'checkbox',
  559. 'label': 'Show Vine column',
  560. 'default': true
  561. },
  562.  
  563. 'FontSize':
  564. {
  565. 'label': 'Text size',
  566. 'type': 'unsigned int',
  567. 'size': 2,
  568. 'default': 12
  569. },
  570.  
  571. 'RowPadding':
  572. {
  573. 'label': 'Row padding',
  574. 'type': 'unsigned int',
  575. 'size': 2,
  576. 'default': 10
  577. },
  578.  
  579. 'HighliteColor':
  580. {
  581. 'label': 'Highlight color (6-place hex code)',
  582. 'title': 'From graphics program or online color picker',
  583. 'type': 'text',
  584. 'size': 6,
  585. 'default': 'FFFF55'
  586. }
  587. },
  588.  
  589. 'events': // Callback functions object
  590. {
  591. 'open': function() {
  592. // style the panel as it's being displayed
  593. frame.style.position = "auto";
  594. frame.style.width = "auto";
  595. frame.style.height = "auto";
  596. frame.style.backgroundColor = "#F3F3F3";
  597. frame.style.padding = "10px";
  598. frame.style.borderWidth = "5px";
  599. frame.style.borderStyle = "ridge";
  600. frame.style.borderColor = "gray";
  601. var x = (document.documentElement.clientWidth - frame.offsetWidth) / 2;
  602. frame.style.left = x + 'px';
  603. }
  604. },
  605.  
  606. 'frame': frame, // specify the DIV element used for the panel
  607.  
  608. 'css': '#MyConfig .config_header { font-size: 12pt; font-weight:bold; margin-bottom:12px }' +
  609. '#MyConfig .field_label { font-size: 12px; font-weight:normal; margin: 0 3px }'
  610. });
  611.  
  612. // event listener to pick up mouse clicks, to run script functions
  613.  
  614. document.addEventListener('click', function(event) {
  615. var tempstr = new String(event.target);
  616. var quash = false;
  617.  
  618. if(tempstr.indexOf('tabulate') > -1) {
  619. tabulate();
  620. quash = true;
  621. }
  622.  
  623. if(tempstr.indexOf('options') > -1) {
  624. GM_config.open();
  625. quash = true;
  626. }
  627.  
  628. if(tempstr.indexOf('toggleView') > -1) {
  629. toggleView();
  630. quash = true;
  631. }
  632.  
  633. if(quash) {
  634. event.stopPropagation();
  635. event.preventDefault();
  636. }
  637. }, true);
  638.  
  639. // initiate the script
  640.  
  641. function main() {
  642.  
  643. var findProfileLink = "";
  644. var url = window.location.href;
  645. // read previous values for footer and top summary values
  646. oldTARTstats = GM_getValue("recentFooterValues", "? ? ? ? ? ? ? ? ? ? ? ? ? ?").split(" ");
  647.  
  648. if(url.indexOf('amazon.com/') > -1) {
  649. // for Amazon US
  650. findProfileLink = document.evaluate("//b[contains(.,'Your Profile')]/a", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  651. }
  652. else {
  653. // Amazon UK, CA, AU
  654. findProfileLink = document.evaluate("//a[contains(.,'Customer reviews')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  655. }
  656. // and, the following lines are ok for US and UK, but, maybe not for others
  657. GM_log("Finding account info...");
  658. var profileLink = findProfileLink.snapshotItem(0).getAttribute("href");
  659. var lst = profileLink.split("/");
  660. userID = lst[4];
  661. GM_log("User ID: " + userID);
  662.  
  663. // find profile info panel
  664. var findDiv = document.evaluate("//div[contains(.,'Helpful Votes')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  665. profileDiv = findDiv.snapshotItem(0);
  666.  
  667. // get helpful votes
  668. var lst = profileDiv.textContent.split(" ");
  669. helpfulVotes = lst[3].substring(7);
  670. GM_log("Helpful Votes: " + helpfulVotes);
  671.  
  672. // get review count
  673. var prevSibDiv = profileDiv.previousElementSibling;
  674. charIdx = prevSibDiv.textContent.lastIndexOf(':');
  675. reviewCount = prevSibDiv.textContent.substring(charIdx+2);
  676. // remove any commas, though has not been necessary w/thousands of reviews
  677. reviewCount = parseInt(reviewCount.replace(/,/g, ''));
  678. GM_log("Review Count: " + reviewCount);
  679.  
  680. // add Tabulate link; also, save content for use with progress indicator
  681. profileDivOriginalHTML = profileDiv.innerHTML;
  682. profileDiv.innerHTML += profileDivTabulateHTML;
  683.  
  684. // add delta symbol with mouseover note, if there are obvious new values to Tabulate
  685. // but, don't show delta on first run, which would have invalid comparison values
  686. if(helpfulVotes != oldTARTstats[7] && invalidValue(oldTARTstats[7]) === false) {
  687. profileDiv.innerHTML += " <a href='javascript: void(0)' style='text-decoration:none; color:#000000' title='Reviewer Ranking or Helpful Vote count has changed since last Tabulate run'>&#916;</a>";
  688. }
  689. }
  690.  
  691. main();
  692.  
  693. })();
  694. // End

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址