AO3 Review + Last Chapter Shortcut + Kudos-sortable Bookmarks

Adds shortcuts for last chapter and a floaty review box, sorts bookmarks by kudos (slow) and allows filter by complete only, download link to listing

  1. // ==UserScript==
  2. // @name AO3 Review + Last Chapter Shortcut + Kudos-sortable Bookmarks
  3. // @namespace saxamaphone
  4. // @version 2.2
  5. // @description Adds shortcuts for last chapter and a floaty review box, sorts bookmarks by kudos (slow) and allows filter by complete only, download link to listing
  6. // @author You
  7. // @match http://archiveofourown.org/*
  8. // @match https://archiveofourown.org/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // Change here to pick what filetype you want the default download to be
  13. var sTypeWanted = 'epub';
  14.  
  15. var oTypeMapping = {
  16. 'mobi': 1,
  17. 'epub': 2,
  18. 'pdf': 3,
  19. 'html': 4
  20. };
  21.  
  22. // From http://stackoverflow.com/a/1909997/584004
  23. (function (jQuery, undefined) {
  24. jQuery.fn.getCursorPosition = function() {
  25. var el = jQuery(this).get(0);
  26. var pos = 0;
  27. if('selectionStart' in el) {
  28. pos = el.selectionStart;
  29. } else if('selection' in document) {
  30. el.focus();
  31. var Sel = document.selection.createRange();
  32. var SelLength = document.selection.createRange().text.length;
  33. Sel.moveStart('character', -el.value.length);
  34. pos = Sel.text.length - SelLength;
  35. }
  36. return pos;
  37. };
  38. })(jQuery);
  39.  
  40. // From http://stackoverflow.com/a/841121/584004
  41. (function (jQuery, undefined) {
  42. jQuery.fn.selectRange = function(start, end) {
  43. if(end === undefined) {
  44. end = start;
  45. }
  46. return this.each(function() {
  47. if('selectionStart' in this) {
  48. this.selectionStart = start;
  49. this.selectionEnd = end;
  50. } else if(this.setSelectionRange) {
  51. this.setSelectionRange(start, end);
  52. } else if(this.createTextRange) {
  53. var range = this.createTextRange();
  54. range.collapse(true);
  55. range.moveEnd('character', end);
  56. range.moveStart('character', start);
  57. range.select();
  58. }
  59. });
  60. };
  61. })(jQuery);
  62.  
  63. // From http://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513#11582513, modified to allow [] in params
  64. function getURLParameter(name) {
  65. return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search.replace(/\[/g, '%5B').replace(/\]/g, '%5D')) || [null, ''])[1].replace(/\+/g, '%20')) || null;
  66. }
  67.  
  68. function getStoryId()
  69. {
  70. var aMatch = window.location.pathname.match(/works\/(\d+)/);
  71. if(aMatch !== null)
  72. return aMatch[1];
  73. else
  74. return jQuery('#chapter_index li form').attr('action').match(/works\/(\d+)/)[1];
  75. }
  76.  
  77. function getBookmarks(sNextPath, aBookmarks, oDeferred) {
  78. jQuery.get(sNextPath, function(oData) {
  79. aBookmarks = jQuery.merge(aBookmarks, jQuery(oData).find('li.bookmark'));
  80. if(jQuery(oData).find('.next a').length)
  81. getBookmarks(jQuery(oData).find('.next').first().find('a').attr('href'), aBookmarks, oDeferred);
  82. else
  83. oDeferred.resolve();
  84. });
  85. }
  86.  
  87. jQuery(window).ready(function() {
  88. // Process bookmarks first because of extra sorting steps. Once this is done, handle everything else
  89. var oBookmarksProcessed = jQuery.Deferred();
  90. // If on the bookmarks page, add option to sort by kudos
  91. if(window.location.pathname.indexOf('/bookmarks') != -1)
  92. {
  93. // Wait to handle the bookmarks after they're loaded
  94. var oBookmarksLoaded = jQuery.Deferred();
  95. var bKudos = false, bComplete = false;
  96. // Add options for Kudos sorting and Complete works only
  97. jQuery('#bookmark_search_sort_column').append('<option value="kudos_count">Kudos</option>');
  98. jQuery('#bookmark_search_with_notes').parent().after('<dt>Status</dt><dd><input id="work_search_complete" name="work_search[complete]" type="checkbox" value="1"/><label for="work_search_complete">Complete only</label></dd>');
  99. if(getURLParameter('bookmark_search%5Bsort_column%5D') == 'kudos_count')
  100. {
  101. jQuery('#bookmark_search_sort_column').val('kudos_count');
  102. bKudos = true;
  103. }
  104. if(getURLParameter('work_search%5Bcomplete%5D') == '1')
  105. {
  106. jQuery('#work_search_complete').attr('checked', 'checked');
  107. bComplete = true;
  108. }
  109. // If either option has been selected, we perform our own process
  110. if(bKudos || bComplete)
  111. {
  112. // Get bookmarks, this takes at least a few seconds so we have to wait for that to finish
  113. var aBookmarks = [];
  114. getBookmarks(window.location.href.replace(/&page=\d+/, ''), aBookmarks, oBookmarksLoaded);
  115. jQuery.when(oBookmarksLoaded).done(function () {
  116. if(bKudos)
  117. {
  118. aBookmarks.sort(function(oA, oB) {
  119. return (parseInt(jQuery(oB).find('dd.kudos').find('a').html()) || 0) - (parseInt(jQuery(oA).find('dd.kudos').find('a').html()) || 0);
  120. });
  121. }
  122. if(bComplete)
  123. {
  124. jQuery.each(aBookmarks, function(iArrayIndex) {
  125. var sChapters = jQuery(this).find('dd.chapters').html();
  126. if(sChapters !== undefined)
  127. {
  128. var aChapters = sChapters.split('\/');
  129. if(aChapters[0] != aChapters[1])
  130. aBookmarks.splice(iArrayIndex, 1);
  131. }
  132. else if (jQuery(this).find('.stats').length === 0)
  133. aBookmarks.splice(iArrayIndex, 1);
  134. });
  135. }
  136.  
  137. var iPage = getURLParameter('page');
  138. if(iPage === null)
  139. iPage = 1;
  140.  
  141. jQuery('li.bookmark').remove();
  142.  
  143. var iIndex;
  144. var iNumBookmarks = aBookmarks.length;
  145. for(iIndex = (iPage-1) * 20; iIndex < (iPage*20) && iIndex < iNumBookmarks; iIndex++)
  146. {
  147. jQuery('ol.bookmark').append(aBookmarks[iIndex]);
  148. }
  149. // If bookmarks are limited by Complete, change the number displayed
  150. if(bComplete)
  151. {
  152. var sPrevHeading = jQuery('h2.heading').html();
  153. jQuery('h2.heading').html(sPrevHeading.replace(/\d+ - \d+ of \d+/, (iPage-1)*20+1 + ' - ' + iIndex + ' of ' + aBookmarks.length));
  154. // Repaginate if necessary
  155. var iFinalPage = jQuery('ol.pagination').first().find('li').not('.previous, .next').last().text();
  156. var iNewFinalPage = Math.ceil(iNumBookmarks/20);
  157. if(iFinalPage > iNewFinalPage)
  158. {
  159. // Rules for AO3 pagination are way too complicated for me to bother replicating, so just going to remove extra pages
  160. var aPageLinks = jQuery('ol.pagination').first().find('li');
  161. jQuery('ol.pagination').find('li a').each(function () {
  162. if(jQuery.isNumeric(jQuery(this).text()) && jQuery(this).text() > iNewFinalPage)
  163. jQuery(this).parent().remove();
  164. });
  165. // Deactivate the last Next link if necessary
  166. if(iPage == iNewFinalPage)
  167. jQuery('ol.pagination').find('li.next').html('<li class="next" title="next"><span class="disabled">Next →</span></li>');
  168. }
  169. }
  170. oBookmarksProcessed.resolve();
  171. });
  172. }
  173. else
  174. oBookmarksProcessed.resolve();
  175. }
  176. else
  177. oBookmarksProcessed.resolve();
  178. jQuery.when(oBookmarksProcessed).done(function() {
  179. // Check if you're on a story or a list
  180. // If not a story page, presume an index page (tags, collections, author, bookmarks, series) and process each work individually, add last chapter link to the end
  181. if(jQuery('.header h4.heading').length)
  182. {
  183. // Near as I can figure, the best way of identifying actual stories in an index page is with the h4 tag with class 'heading' within a list of type 'header'
  184. jQuery('.header h4.heading').each(function() {
  185. var sStoryPath = jQuery(this).find('a').first().attr('href');
  186. var oHeader = this;
  187.  
  188. // If link is from collections, get proper link
  189. var aMatch = sStoryPath.match(/works\/(\d+)/);
  190. if(aMatch !== null)
  191. {
  192. var iStoryId = aMatch[1];
  193. console.log('hi');
  194. // Access first chapter of story to get last chapter and download links
  195. jQuery.get('https://archiveofourown.org/works/' + iStoryId, function(oData) {
  196. console.log(oData);
  197. var iLastChapterId = jQuery(oData).find('#selected_id option').last().val();
  198. jQuery(oHeader).append(' <a href="/works/' + iStoryId + '/chapters/' + iLastChapterId +'" title="Jump to last chapter">»</a>');
  199. // Use the chosen filetype from the beginning
  200. var sDownloadLink = jQuery(oData).find('.download ul li:nth-child(' + oTypeMapping[sTypeWanted] + ') a').attr('href');
  201. console.log('link: '+sDownloadLink);
  202. jQuery(oHeader).append(' <a href="' + sDownloadLink + '" title="Download work">↡</a>');
  203. }).fail(function() {
  204. console.log('failed');
  205. });
  206. }
  207. });
  208. }
  209. // Review box and last chapter buttons are story-specific
  210. else if(jQuery('ul.work'))
  211. {
  212. // HTML to define layout of popup box
  213. // Include x button to close box
  214. var sHtml = '<p class="close actions" id="close_floaty"><a aria-label="cancel" style="display: inline-block;">×</a></p>';
  215. // Button to insert highlighted text and for a help list
  216. sHtml += '<ul class="actions" style="float: left; margin-top: 10px;"><li id="insert_floaty_text"><a>Insert</a></li><li id="pop_up_review_tips"><a>Review Tips</a></li></ul>';
  217. // Textarea
  218. sHtml += '<textarea style="margin: 5px; width: 99%;" id="floaty_textarea"></textarea>';
  219.  
  220. // Create popup box
  221. jQuery("<div/>", {
  222. id: "reviewTextArea",
  223. width:600, // Change for dimensions
  224. height:300, // Change for dimensions
  225. css: {
  226. backgroundColor:"#ffffff",
  227. opacity: 0.75,
  228. border: "thin solid black",
  229. display: "inline-block",
  230. "padding-right": 10,
  231. position: "fixed",
  232. top: 150,
  233. right: 5
  234. },
  235. html: sHtml
  236. }).resizable().draggable().appendTo("body");
  237.  
  238. // Hide the popup box by default (comment out line below if you want it to always appear by adding // before it)
  239. jQuery('#reviewTextArea').hide();
  240.  
  241. // To close the box
  242. jQuery('#close_floaty').click(function() {
  243. jQuery('#reviewTextArea').hide();
  244. });
  245.  
  246. // Anything you type in the box gets inserted into the real comment box below
  247. jQuery('#floaty_textarea').on('input', function() {
  248. jQuery('.comment_form').val(jQuery('#floaty_textarea').val());
  249. });
  250.  
  251. // Add Float review box button to the top
  252. jQuery('ul.work').prepend('<li id="floaty_review_box"><a>Floaty Review Box</a></li>');
  253.  
  254. // If the above button is clicked, display the review box
  255. jQuery('#floaty_review_box').click(function() {
  256. jQuery('#reviewTextArea').show();
  257. });
  258.  
  259. // Insert highlighted/selected text into textarea when Insert button is clicked
  260. jQuery('#insert_floaty_text').click(function() {
  261. var sInitialText = jQuery('#floaty_textarea').val();
  262. var iPosition = jQuery('#floaty_textarea').getCursorPosition();
  263.  
  264. var sHighlightedText = window.getSelection().toString();
  265.  
  266. var sNewText = sInitialText.substr(0, iPosition) + '<i>"' + sHighlightedText + '"</i>\n' + sInitialText.substr(iPosition);
  267. jQuery('#floaty_textarea').val(sNewText);
  268. jQuery('#floaty_textarea').focus();
  269. jQuery('#floaty_textarea').selectRange(iPosition+sHighlightedText.length+10);
  270.  
  271. // Copy into real comment box
  272. jQuery('.comment_form').val(jQuery('#floaty_textarea').val());
  273. });
  274. // Create the review tips box
  275. sReviewTipsHtml = '<p class="close actions" id="close_review_tips"><a aria-label="cancel" style="display: inline-block;">×</a></p>' +
  276. 'Writers will love any love you give them. If you&#39;re looking for things to help jumpstart a review, there are lots of different things you could focus on.<br />' +
  277. '<ul><li>Quotes you liked</li><li>Scenes you liked</li><li>What&#39;s your feeling at the end of the chapter (did it move you?)</li><li>What are you most looking forward to next?</li>' +
  278. '<li>Do you have any predictions for the next chapters you want to share?</li><li>Did this chapter give you any questions you can&#39;t wait to find out the answers for?</li>' +
  279. '<li>How would you describe the fic to a friend if you were recommending it?</li><li>Is there something unique about the story that you like?</li><li>Does the author have a style that really works for you?</li>' +
  280. '<li>Did the author leave any comments in the notes that said what they wanted feedback on?</li>' +
  281. '<li>Even if all you have are &quot;incoherent screams of delight&quot;, and can&#39;t come up with a real comment at the moment, authors love to hear that as well</li></ul>';
  282. jQuery("<div/>", {
  283. id: "reviewTips",
  284. width:600, // Change for dimensions
  285. height:300, // Change for dimensions
  286. css: {
  287. backgroundColor:"#ffffff",
  288. border: "thin solid black",
  289. 'font-size': '80%',
  290. padding: '10px 10px 0 10px',
  291. position: "fixed",
  292. top: 150,
  293. right: 620
  294. },
  295. html: sReviewTipsHtml
  296. }).resizable().draggable().appendTo("body");
  297. jQuery('#reviewTips li').css('list-style', 'circle inside none');
  298. jQuery('#reviewTips').hide();
  299. // Pop up list of review tips
  300. jQuery('#pop_up_review_tips').click(function() {
  301. jQuery('#reviewTips').show();
  302. });
  303. jQuery('#close_review_tips').click(function() {
  304. jQuery('#reviewTips').hide();
  305. });
  306.  
  307. // Before adding button for Last Chapter, make sure we're not on the last (or only) chapter already
  308. if(jQuery('.next').length)
  309. {
  310. // Add button for Last Chapter
  311. jQuery('ul.work').prepend('<li id="go_to_last_chap"><a>Last Chapter</a></li>');
  312.  
  313. // If the above button is clicked, go to last chapter
  314. jQuery('#go_to_last_chap').click(function() {
  315. window.location.href = '/works/' + getStoryId() + '/chapters/' + jQuery('#selected_id option').last().val();
  316. });
  317. }
  318.  
  319. // Adding a First Chapter button
  320. if(jQuery('.previous').length)
  321. {
  322. // Add button for First Chapter
  323. jQuery('ul.work').prepend('<li id="go_to_first_chap"><a>First Chapter</a></li>');
  324.  
  325. // If the above button is clicked, go to first chapter
  326. jQuery('#go_to_first_chap').click(function() {
  327. window.location.href = '/works/' + getStoryId();
  328. });
  329. }
  330. }
  331. });
  332. });