AO3: Tracking

Track any filterable listing.

  1. // ==UserScript==
  2. // @name AO3: Tracking
  3. // @description Track any filterable listing.
  4. // @namespace https://greasyfork.org/en/scripts/8382-ao3-tracking
  5. // @author Min
  6. // @version 1.5
  7. // @grant none
  8. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js
  9. // @include http://*archiveofourown.org/*
  10. // @include https://*archiveofourown.org/*
  11. // ==/UserScript==
  12.  
  13.  
  14. (function($) {
  15.  
  16. if (typeof(Storage) !== 'undefined') {
  17.  
  18. var tracked_list = '';
  19. var tracked_array = [];
  20.  
  21. loadList();
  22.  
  23. addCss();
  24. addTrackedMenu();
  25.  
  26. var main = $('#main');
  27.  
  28. // if it's a listing of works or bookmarks
  29. if (main.hasClass('works-index') || main.hasClass('bookmarks-index')) {
  30.  
  31. var is_tracked = false;
  32. var is_first_page = true;
  33.  
  34. // get page url
  35. var location_url = location.href;
  36.  
  37. // check if page is already tracked
  38. var array_index = tracked_array.indexOf(location_url);
  39. if (array_index > -1) {
  40. is_tracked = true;
  41.  
  42. checkOpen();
  43. }
  44.  
  45. // make sure it's the first page of the listing
  46. var current_page = main.find('ol.pagination:first span.current');
  47. if (current_page.length && current_page.text() !== '1') {
  48. is_first_page = false;
  49. }
  50.  
  51. if (is_first_page) { addTrackButton(); }
  52. }
  53.  
  54. // add "Mark all as vieved" button when all listing get checked
  55. $(document).ajaxStop(function() {
  56. if ($('#tracked-box').length && !$('#button-check').length) {
  57. $('#button-mark').css('visibility', 'visible');
  58. }
  59. });
  60. }
  61.  
  62. // load the saved list
  63. function loadList() {
  64.  
  65. tracked_list = localStorage.getItem('ao3tracking_list');
  66. if (!tracked_list) { tracked_list = ''; }
  67.  
  68. // make an array of the list
  69. if (tracked_list.length) {
  70. tracked_array = tracked_list.split(',,,');
  71. }
  72. }
  73.  
  74. // add current page to tracked listings
  75. function addToTracked() {
  76.  
  77. var added = false;
  78.  
  79. loadList();
  80.  
  81. // if there's less than 25 tracked
  82. if (tracked_array.length < 75) {
  83.  
  84. // ask for name
  85. var heading = main.find('h2.heading:first');
  86. var heading_link = heading.find('a');
  87.  
  88. if (heading_link.length) {
  89. var suggest = heading_link.text();
  90. }
  91. else {
  92. var suggest = heading.text().replace(/\n/g, '').replace(/^\s+/, '').replace(/(.+of )?\d+ /, '');
  93. }
  94.  
  95. var listing_name = prompt('Name for the tracked listing:', suggest);
  96.  
  97. if (listing_name !== '' && listing_name !== null) {
  98.  
  99. // remove characters we don't want in the names
  100. listing_name = listing_name.replace(/,,,/g, ' ');
  101.  
  102. var listing_count = heading.text().replace(/\n/g, '').replace(/^\s+/, '').replace(/.*\d+ - \d+ of /, '').replace(/(\d+)(.+)/, '$1');
  103.  
  104. // add name, url, count
  105. tracked_array.push(listing_name, location_url, listing_count);
  106. tracked_list = tracked_array.join(',,,');
  107.  
  108. // save the updated list
  109. localStorage.setItem('ao3tracking_list', tracked_list);
  110.  
  111. added = true;
  112. }
  113. }
  114. else {
  115. alert("You're already tracking 25 listings. Remove some first.");
  116. }
  117.  
  118. return added;
  119. }
  120.  
  121. // remove a given url from tracked listings
  122. function removeFromTracked(url) {
  123.  
  124. var removed = false;
  125.  
  126. loadList();
  127.  
  128. var index = tracked_array.indexOf(url);
  129.  
  130. // if the url is on the saved list
  131. if (index > -1) {
  132.  
  133. // ask for confirmation
  134. var confirmed = confirm('Sure you want to remove "' + tracked_array[index-1] + '"?');
  135.  
  136. if (confirmed) {
  137. // remove name, url, count
  138. tracked_array.splice(index-1, 3);
  139. tracked_list = tracked_array.join(',,,');
  140.  
  141. // save the updated list
  142. localStorage.setItem('ao3tracking_list', tracked_list);
  143.  
  144. removed = true;
  145. }
  146. }
  147.  
  148. return removed;
  149. }
  150.  
  151. // check open page for new works
  152. function checkOpen() {
  153.  
  154. var heading = main.find('h2.heading:first');
  155.  
  156. // get a count of new works
  157. var current_count = getCountFromHeading(heading.text());
  158. var saved_count = parseInt(tracked_array[array_index+1]);
  159. var new_count = current_count - saved_count;
  160.  
  161. if (new_count !== 0) {
  162.  
  163. heading.append(' <span id="new-works">(' + new_count + ' new)</span> <a id="mark-viewed">[mark viewed]</a>');
  164.  
  165. $('#mark-viewed').click(function() {
  166.  
  167. loadList();
  168.  
  169. var array_index = tracked_array.indexOf(location_url);
  170. if (array_index > -1) {
  171. // update the count
  172. tracked_array[array_index+1] = current_count;
  173. tracked_list = tracked_array.join(',,,');
  174.  
  175. // save the updated list
  176. localStorage.setItem('ao3tracking_list', tracked_list);
  177. }
  178.  
  179. $('#new-works').detach();
  180. $(this).detach();
  181. });
  182. }
  183. }
  184.  
  185. // check the tracked listings for new works
  186. function checkForNew() {
  187.  
  188. // check if it's more than 8 hours since last check
  189. var last_check = localStorage.getItem('ao3tracking_lastcheck');
  190. if (!last_check) { var last_check = 0; }
  191. else { last_check = parseInt(last_check); }
  192.  
  193. var now = new Date();
  194. now = now.getTime();
  195. var wait = 28800000 - (now - last_check);
  196.  
  197. if (wait < 0) {
  198.  
  199. localStorage.setItem('ao3tracking_lastcheck', now);
  200.  
  201. // for each tracked listing
  202. $('#tracked-box li.tracked-listing').each(function() {
  203.  
  204. var tracked_url = $(this).find('a').attr('href');
  205. var listing_id = $(this).attr('id');
  206.  
  207. tracked_url += ' #main h2.heading:first';
  208.  
  209. // load heading of the tracked page
  210. $(this).find('span.tracked-current').load(tracked_url, function() {
  211.  
  212. var listing = $('#' + listing_id);
  213.  
  214. // get a count of new works
  215. var current_count = getCountFromHeading(listing.find('span.tracked-current').text());
  216. listing.find('span.tracked-current').html(current_count);
  217. var saved_count = parseInt(listing.find('span.tracked-saved').text());
  218. var new_count = current_count - saved_count;
  219.  
  220. listing.find('span.tracked-new').text('(' + new_count + ' new)');
  221.  
  222. if (new_count !== 0) {
  223. listing.find('span.tracked-new').addClass('new-stuff');
  224. listing.parent().prepend(listing);
  225. }
  226. else {
  227. listing.find('span.tracked-new').addClass('no-new-stuff');
  228. }
  229. });
  230. });
  231. }
  232. else {
  233. var hours = Math.floor(wait/3600000);
  234. var minutes = Math.ceil((wait%3600000)/60000);
  235.  
  236. var warning = $('<p style="color: #990000;"></p>');
  237.  
  238. if (hours > 0) {
  239. warning.html('<strong>Please be kind to the AO3 servers!</strong> Wait ' + hours + ' hour(s) and ' + minutes + ' minute(s) more before another check.');
  240. }
  241. else {
  242. warning.html('<strong>Please be kind to the AO3 servers!</strong> Wait ' + minutes + ' more minute(s) before another check.');
  243. }
  244.  
  245. $('#tracked-box p.actions').after(warning);
  246. }
  247. }
  248.  
  249. // add the 'Track This' button
  250. function addTrackButton() {
  251.  
  252. var work_filters = $('form.filters, form.old-filters').find('dd.submit.actions:first');
  253.  
  254. var track_this_button = $('<input type="button" value="Track This" class="track-this"></input>');
  255. track_this_button.click(function() {
  256. var added = addToTracked();
  257. if (added) {
  258. track_this_button.detach();
  259. work_filters.prepend(untrack_this_button);
  260. }
  261. });
  262.  
  263. var untrack_this_button = $('<input type="button" value="Untrack This" class="track-this"></input>');
  264. untrack_this_button.click(function() {
  265. var removed = removeFromTracked(location_url);
  266. if (removed) {
  267. untrack_this_button.detach();
  268. work_filters.prepend(track_this_button);
  269. }
  270. });
  271.  
  272. // if the page is already tracked
  273. if (is_tracked) {
  274. work_filters.prepend(untrack_this_button);
  275. }
  276. // if it's not tracked
  277. else {
  278. work_filters.prepend(track_this_button);
  279. }
  280. }
  281.  
  282. // rearrange things on the list
  283. function editList() {
  284.  
  285. var box_list = $('#box-list');
  286.  
  287. box_list.find('li.tracked-listing').each(function() {
  288. $(this).prepend('<span class="up-arrow clickable">&uarr;</span> <span class="down-arrow clickable">&darr;</span> <span class="cross clickable">&cross;</span> ');
  289. });
  290.  
  291. box_list.on('click', 'span.up-arrow', function() {
  292. $(this).parent().prev().before($(this).parent());
  293. });
  294.  
  295. box_list.on('click', 'span.down-arrow', function() {
  296. $(this).parent().next().after($(this).parent());
  297. });
  298.  
  299. box_list.on('click', 'span.cross', function() {
  300. $(this).parent().detach();
  301. });
  302. }
  303.  
  304. // save list after edits
  305. function saveList() {
  306.  
  307. tracked_array = [];
  308.  
  309. // get name, url, count for all listings
  310. $('#tracked-box li.tracked-listing').each(function() {
  311.  
  312. var name = $(this).find('a').text();
  313. var url = $(this).find('a').attr('href');
  314. var count = $(this).find('span.tracked-saved').text();
  315.  
  316. tracked_array.push(name, url, count);
  317. });
  318.  
  319. // update and save the new list
  320. tracked_list = tracked_array.join(',,,');
  321. localStorage.setItem('ao3tracking_list', tracked_list);
  322.  
  323. // reload the box
  324. $('#tracked-box').detach();
  325. $('#tracked-bg').detach();
  326. showBox();
  327. }
  328.  
  329. // update the listings counts
  330. function markAllViewed() {
  331.  
  332. loadList();
  333.  
  334. // get the current count for all listings
  335. $('#tracked-box li.tracked-listing').each(function() {
  336.  
  337. var url = $(this).find('a').attr('href');
  338. var current_count = $(this).find('span.tracked-current').text();
  339.  
  340. var index = tracked_array.indexOf(url);
  341. if (index > -1) {
  342. tracked_array[index+1] = current_count;
  343. }
  344. });
  345.  
  346. // update and save the new list
  347. tracked_list = tracked_array.join(',,,');
  348. localStorage.setItem('ao3tracking_list', tracked_list);
  349.  
  350. // reload the box
  351. $('#tracked-box').detach();
  352. $('#tracked-bg').detach();
  353. showBox();
  354. }
  355.  
  356. // show the box with tracked listings
  357. function showBox() {
  358.  
  359. var tracked_bg = $('<div id="tracked-bg"></div>');
  360.  
  361. var tracked_box = $('<div id="tracked-box"></div>');
  362.  
  363. var box_buttons = $('<p class="actions"></p>');
  364.  
  365. var box_button_check = $('<input type="button" id="button-check" value="Check for new"></input>');
  366. box_button_check.click(function() {
  367. box_button_edit.after(box_button_mark);
  368. checkForNew();
  369. box_button_edit.detach();
  370. box_button_check.detach();
  371. });
  372.  
  373. var box_button_edit = $('<input type="button" id="button-edit" value="Edit list"></input>');
  374. box_button_edit.click(function() {
  375. editList();
  376. box_button_edit.after(box_button_save, box_button_cancel);
  377. box_button_check.detach();
  378. box_button_edit.detach();
  379. });
  380.  
  381. var box_button_save = $('<input type="button" id="button-save" value="Save list"></input>');
  382. box_button_save.click(function() { saveList(); });
  383.  
  384. var box_button_cancel = $('<input type="button" id="button-cancel" value="Cancel edits"></input>');
  385. box_button_cancel.click(function() {
  386. tracked_box.detach();
  387. tracked_bg.detach();
  388. showBox();
  389. });
  390.  
  391. var box_button_mark = $('<input type="button" id="button-mark" style="visibility: hidden;" value="Mark all as viewed"></input>');
  392. box_button_mark.click(function() { markAllViewed(); });
  393.  
  394. var box_button_close = $('<input type="button" id="button-close" value="Close"></input>');
  395. box_button_close.click(function() {
  396. tracked_box.detach();
  397. tracked_bg.detach();
  398. });
  399.  
  400. var box_header = $('<h3></h3>').text('Tracked listings [' + tracked_array.length/3 + '/25]:');
  401. var box_list = $('<ul id="box-list"></ul>');
  402.  
  403. tracked_box.append(box_buttons, box_header, box_list);
  404.  
  405. // if there are saved listings
  406. if (tracked_array.length > 2) {
  407. for (var i = 0; i < tracked_array.length; i += 3) {
  408.  
  409. var listing = $('<li id="tracked-listing-' + i/3 + '" class="tracked-listing"></li>').html('<a href="' + tracked_array[i+1] + '">' + tracked_array[i] + '</a> <span class="tracked-new"></span> <span class="tracked-saved">' + tracked_array[i+2] + '</span> <span class="tracked-current"></span>');
  410. box_list.append(listing);
  411. }
  412. }
  413. else {
  414. var no_listings = $('<li style="opacity: 0.5; font-style: oblique;"></li>').html("you're not tracking anything yet!");
  415. box_list.append(no_listings);
  416.  
  417. box_button_check.css('visibility', 'hidden');
  418. box_button_edit.css('visibility', 'hidden');
  419. }
  420.  
  421. box_buttons.append(box_button_check, box_button_edit, box_button_close);
  422.  
  423. $('body').append(tracked_bg, tracked_box);
  424. }
  425.  
  426. // attach the menu
  427. function addTrackedMenu() {
  428.  
  429. // get the header menu
  430. var header_menu = $('ul.primary.navigation.actions');
  431.  
  432. // create and insert menu button
  433. var tracked_button = $('<input class="button" type="button" value="Tracked"></input>');
  434. header_menu.find('#search').prepend(tracked_button);
  435. tracked_button.click(function() {
  436. if ($('#tracked-box').length == 0) {
  437. loadList();
  438. showBox();
  439. }
  440. });
  441. }
  442.  
  443. // parse heading for works count
  444. function getCountFromHeading(heading_text) {
  445. try {
  446. return parseInt(heading_text.replace(/\n/g, '').replace(/^\s+/, '').replace(/,/g, '').replace(/.*\d+ - \d+ of /, '').replace(/(\d+)(.+)/, '$1'));
  447. }
  448. catch (e) {
  449. return 0;
  450. }
  451. }
  452.  
  453. // add css rules to page head
  454. function addCss() {
  455. var style = $('<style type="text/css"></style>').appendTo($('head'));
  456.  
  457. var css = '#tracked-box {position: fixed; top: 0px; bottom: 0px; left: 0px; right: 0px; width: 60%; height: 80%; max-width: 800px; margin: auto; overflow-y: auto; border: 10px solid #eee; box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2); padding: 0 20px; background-color: #ffffff; z-index: 999;}\
  458. #tracked-bg {position: fixed; width: 100%; height: 100%; background-color: #000000; opacity: 0.7; z-index: 998;}\
  459. input[type="button"] {height: auto;}\
  460. .filters input.track-this {margin-bottom: 0; width: 100%;}\
  461. .old-filters input.track-this {margin-bottom: 10px;}\
  462. #tracked-box p.actions {float: none; text-align: left;}\
  463. #button-save {font-weight: bold;}\
  464. #button-close {float: right;}\
  465. #tracked-box li span.tracked-new.new-stuff {font-weight: bold;}\
  466. #tracked-box li span.tracked-new.no-new-stuff {opacity: 0.5;}\
  467. #tracked-box li span.tracked-current, #tracked-box li span.tracked-saved {display: none;}\
  468. #tracked-box li .clickable {cursor: pointer; margin-right: 7px;}\
  469. #new-works {font-weight: bold;}';
  470.  
  471. style.append(css);
  472. }
  473. })(jQuery);