AO3: [Wrangling] Fandom Resources Quicklinks

adds a bar with fandom-specific links at the top of the bin

当前为 2024-10-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AO3: [Wrangling] Fandom Resources Quicklinks
  3. // @namespace https://greasyfork.org/en/users/906106-escctrl
  4. // @description adds a bar with fandom-specific links at the top of the bin
  5. // @author escctrl
  6. // @version 1.1
  7. // @match *://*.archiveofourown.org/tags/*/wrangle?*
  8. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
  9. // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js
  11. // @require https://update.greasyfork.org/scripts/491888/1355841/Light%20or%20Dark.js
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. /* global jQuery, lightOrDark */
  16.  
  17. (function($) {
  18. 'use strict';
  19.  
  20. // --- THE USUAL INIT STUFF AT THE BEGINNING -------------------------------------------------------------------------------
  21.  
  22. $("head").append(`<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">`);
  23.  
  24. let cfg = 'wrangleResources'; // name of dialog and localstorage used throughout
  25. let dlg = '#'+cfg;
  26.  
  27. /* *** EXAMPLE STORAGE: all Witcher (franchise nickname) subfandoms are listed in [0] to use the resources listed in [1]ff
  28. resources = {
  29. "Witcher" : [ ["Wiedźmin | The Witcher - All Media Types", "Wiedźmin | The Witcher (Video Game)", "Wiedźmin | The Witcher Series - Andrzej Sapkowski", "The Witcher (TV)"],
  30. ["wikia", "https://witcher.fandom.com/wiki/Witcher_Wiki"],
  31. ["IMDB Cast", "https://www.imdb.com/title/tt5180504/fullcredits/"] ],
  32. };
  33. */
  34. let resources = loadConfig();
  35.  
  36. // --- CONFIGURATION DIALOG HANDLING -------------------------------------------------------------------------------
  37.  
  38. createDialog();
  39.  
  40. function createDialog() {
  41.  
  42. // if the background is dark, use the dark UI theme to match
  43. let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "dark-hive" : "base";
  44.  
  45. // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling
  46. $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
  47. .append(`<style tyle="text/css">${dlg}, .ui-dialog .ui-dialog-buttonpane button {font-size: revert; line-height: 1.286;}
  48. ${dlg} form {box-shadow: revert; cursor:auto;}
  49. ${dlg} fieldset {background: revert; box-shadow: revert;}
  50. ${dlg} fieldset p { padding-left: 0; padding-right: 0; }
  51. ${dlg} legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
  52. ${dlg} fieldset input::placeholder { font-style: italic; opacity: 0.2; }
  53. ${dlg} fieldset input[type="text"] { padding: 0.2em 0.5em; width: 20em; }
  54. ${dlg} fieldset input[type="text"][id^="display"] { width: 10em; }
  55. ${dlg} fieldset p.indented { padding: 0.2em 0 0.2em 3em; }
  56. ${dlg} fieldset p.linked { display: table; width: 100%; }
  57. ${dlg} fieldset p.linked label { display: table-cell; width: 4em; vertical-align: top; padding-top: 0.2em; }
  58. ${dlg} fieldset p.linked ul { display: table-cell; width: auto; }
  59. ${dlg} fieldset div.fandom, ${dlg} fieldset div.fandom-new { margin: 1em 0 0 0; }
  60. ${dlg} fieldset button { margin: 0.1em; }
  61. ${dlg} fieldset ul.autocomplete li { margin: 0.2em; }
  62. </style>`);
  63.  
  64. // wrapper div for the dialog
  65. $("#main").append(`<div id="${cfg}"></div>`);
  66.  
  67. let prevStoredHTML = "";
  68. for (let [f, r] of Object.entries(resources)) {
  69. prevStoredHTML += templateFandom(f, r);
  70. }
  71.  
  72. $(dlg).html(`<form>
  73. <fieldset><legend>Fandom Resources</legend>
  74. ${prevStoredHTML}
  75. <div class="fandom-new"><button name="add-fandom" type="button"><i class="fa fa-plus-square" aria-hidden="true"></i> Add Fandom</button></div>
  76. </fieldset>
  77. </form>`);
  78.  
  79. // optimizing the size of the GUI in case it's a mobile device
  80. let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px)
  81. if (dialogwidth < 1000) $("head").append(`<style tyle="text/css"> ${dlg} label { display: none; } </style>`); // saving some space on narrow screens
  82. dialogwidth = dialogwidth > 500 ? dialogwidth * 0.7 : dialogwidth * 0.9;
  83. let dialogheight = parseFloat(getComputedStyle($(dlg)[0]).fontSize) * 50;
  84.  
  85. $(dlg).dialog({
  86. appendTo: "#main",
  87. modal: true,
  88. title: 'Quicklinks to Fandom Resources Config',
  89. draggable: true,
  90. resizable: false,
  91. autoOpen: false,
  92. width: dialogwidth,
  93. maxHeight: dialogheight,
  94. position: {my:"center", at: "center top"},
  95. buttons: {
  96. Reset: deleteConfig,
  97. Save: storeConfig,
  98. Cancel: function() { $( dlg ).dialog( "close" ); }
  99. }
  100. });
  101.  
  102. // if no other script has created it yet, write out a "Userscripts" option to the main navigation
  103. if ($('#scriptconfig').length == 0) {
  104. $('#header ul.primary.navigation li.dropdown').last()
  105. .after(`<li class="dropdown" id="scriptconfig">
  106. <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
  107. <ul class="menu dropdown-menu"></ul></li>`);
  108. }
  109. // then add this script's config option to navigation dropdown
  110. $('#scriptconfig .dropdown-menu').append(`<li><a href="javascript:void(0);" id="opencfg_${cfg}">Fandom Resources</a></li>`);
  111.  
  112. // on click, open the configuration dialog
  113. $("#opencfg_"+cfg).on("click", function(e) {
  114. $( dlg ).dialog('open');
  115. });
  116.  
  117. }
  118.  
  119. // --- DELEGATED EVENT HANDLERS FOR REACTIVE GUI -------------------------------------------------------------------------------
  120.  
  121. // adding/removing fandoms/resources/links
  122. $(dlg).on("click", "fieldset button", function(e) {
  123. e.preventDefault();
  124.  
  125. // these create the necessary blank HTML fields that get dynamically added on button-clicks
  126. let newresource = templateResource();
  127. let newfandom = templateFandom();
  128.  
  129. let parent = $(e.target).parent();
  130.  
  131. // depending on the button that was clicked, we add/remove different rows of data or hide buttons
  132. switch (e.target.name) {
  133. case "add-resource":
  134. $(parent).before(newresource);
  135. e.target.scrollIntoView(false);
  136. break;
  137. case "delete-resource":
  138. $(parent).remove();
  139. break;
  140. case "delete-fandom":
  141. $(parent).parent().remove();
  142. break;
  143. case "add-fandom":
  144. $(parent).before(newfandom);
  145. e.target.scrollIntoView(false);
  146. break;
  147. }
  148. });
  149.  
  150. // --- HELPER FUNCTIONS TO CREATE GUI HTML -------------------------------------------------------------------------------
  151.  
  152. // creates a whole block for a fandom, with the <input> for a user-defined nickname, plus the linked fandoms and resources
  153. function templateFandom(f = "", r = [[""], ["", ""]]) {
  154. let resourcesHTML = ""; // holds HTML of the resource/fandom link configuration that was stored for the given fandom
  155. let myr = [...r]; // need to deep-copy this because we're later shifting the first element off, which would affect the original resources config
  156.  
  157. // list the fandoms that were previously entered (or a field for entering a new one)
  158. resourcesHTML = templateLink(myr[0].join(","));
  159. myr.shift(); // remove the fandoms at [0] from our array
  160.  
  161. // when a bunch of resource links were configured for this fandom, we build those as HTML & hide the fandom-link button
  162. for (let entry of myr) { resourcesHTML += templateResource(entry); }
  163.  
  164. return `
  165. <div class="fandom">
  166. <label for="fandom[]">Title:</label>
  167. <input type="text" id="fandom[]" name="fandom[]" placeholder="of fandom or franchise" value="${f}"/>
  168. ${resourcesHTML}
  169. <p class="indented add-new">
  170. <button name="add-resource" type="button"><i class="fa fa-plus-square-o" aria-hidden="true"></i> Add Resource</button>
  171. <button name="delete-fandom" type="button"><i class="fa fa-trash" aria-hidden="true"></i> Delete Fandom Config</button>
  172. </p>
  173. </div>`;
  174. }
  175.  
  176. // creates a line with two <input> fields to enter the resource display text and URL, and a button to remove that line again
  177. function templateResource(r = ["", ""]) {
  178. return `
  179. <p class="indented resource">
  180. <i class="fa fa-external-link" aria-hidden="true"></i>
  181. <label for="display[]">Resource:</label> <input type="text" id="display[]" name="display[]" placeholder="e.g. IMDB" value="${r[0]}" />
  182. <label for="url[]">URL:</label> <input type="text" id="url[]" name="url[]" placeholder="e.g. http://www.imdb.com" value="${r[1]}" />
  183. <button name="delete-resource" type="button"><i class="fa fa-minus-square-o" aria-hidden="true"></i> Remove Resource</button>
  184. </p>`;
  185. }
  186.  
  187. // creates an AO3-standard autocomplete textfield for selecting fandoms, which is prepopulated with previously stored fandoms
  188. function templateLink(l = "") {
  189. return `
  190. <p class="indented linked">
  191. <label for="linked[]"><i class="fa fa-hand-o-right" aria-hidden="true"></i> Bins:</label>
  192. <input type="text" id="linked[]" name="linked[]" class="fandom autocomplete" data-autocomplete-method="/autocomplete/fandom"
  193. data-autocomplete-hint-text="Start typing for Fandom suggestions!" data-autocomplete-no-results-text="(No suggestions found)"
  194. data-autocomplete-min-chars="1" data-autocomplete-searching-text="Searching..." value="${l}"/>
  195. </p>`;
  196. }
  197.  
  198. // --- LOCALSTORAGE MANIPULATION -------------------------------------------------------------------------------
  199.  
  200. function deleteConfig() {
  201. if (confirm('Are you sure you want to delete all Fandom Resource quicklinks?')) {
  202. localStorage.removeItem(cfg);
  203. $(dlg).dialog('close');
  204. // currently this is creating a "n.slice is not a function" exception. not a clue why. none of the other ways to close the dialog have issues.
  205. }
  206. }
  207.  
  208. function storeConfig() {
  209. // object to start collecting our storage data
  210. let fandom_resources = {};
  211.  
  212. let errors = [];
  213.  
  214. // grab all the elements and fields
  215. $(dlg).find('div.fandom').each(function(ix) {
  216. let nickname = $(this).find('input[name="fandom[]"]').prop('value');
  217.  
  218. if (nickname.length > 0) {
  219.  
  220. let linkedfandoms = $(this).find('p.linked ul.autocomplete li.added.tag');
  221. let resources = $(this).find('p.resource');
  222.  
  223. if ($(linkedfandoms).length > 0) {
  224. fandom_resources[nickname] = [];
  225. // gather all the selected fandoms' canonical tagnames together into an array
  226. console.log(linkedfandoms.map(function() { return $(this).contents().eq(0).text().trim(); }));
  227. fandom_resources[nickname][0] = $(linkedfandoms).map(function() { return $(this).contents().eq(0).text().trim(); }).toArray();
  228.  
  229. if ($(resources).length > 0) {
  230. $(resources).each(function() {
  231. let display = $(this).find('input[id="display[]"]').prop('value') || "";
  232. let url = $(this).find('input[id="url[]"]').prop('value') || "";
  233.  
  234. if (url !== "" && display === "") display = "link"; // set default display text if there is a URL
  235.  
  236. // we don't keep completely empty resource lines, otherwise we store a resource
  237. if (url === "" && display === "") errors.push(`a resource for fandom ${nickname} won't be stored, no link text nor URL given`);
  238. else fandom_resources[nickname].push([display, url]);
  239. });
  240. }
  241. }
  242. else errors.push(`resources for fandom ${nickname} won't be stored, no fandoms were linked to use them`);
  243. }
  244. else errors.push(`entry #${ix+1} won't be stored, no Title given`);
  245. });
  246.  
  247. if (errors.length > 0) console.log("Some Fandom Resource entries could not be stored because they're missing data:\n" + errors.join("\n"));
  248. // by the end of this, we've filled up fandom_resources with all data and are ready to store
  249. localStorage.setItem(cfg, JSON.stringify(fandom_resources));
  250. $(dlg).dialog('close');
  251. }
  252.  
  253. function loadConfig() {
  254. return JSON.parse(localStorage.getItem(cfg) ?? "{}");
  255. }
  256.  
  257. // --- WRITING THE TOP BAR WITH THE WRANGLING RESOURCE LINKS -------------------------------------------------------------------------------
  258.  
  259. // if this isn't a fandom bin, quit because we don't know the fandom to show resources for
  260. // also quit if there are no tags to display: mostly because we rely on #wrangulator to provide the styling
  261. if ($('#inner').find('ul.navigation.actions').eq(1).find('li').length != 5 || $('#wrangulator').length < 1) return;
  262.  
  263. // grab the currently viewed fandom name
  264. let fandom = $('#main > .heading a.tag').text();
  265.  
  266. // now we try to find the fandom in the storage
  267. let links = [];
  268. for (var nick of Object.values(resources)) {
  269. if (nick[0].includes(fandom)) {
  270. nick.slice(1).forEach((val) => links.push(`<a href="${val[1]}" target="_blank">${val[0]} <i class="fa fa-external-link" aria-hidden="true"></i></a>`));
  271. break;
  272. }
  273. }
  274.  
  275. if (links.length > 0) {
  276.  
  277. let bgcolor = $('#wrangulator fieldset').css('background-color');
  278. let fontcolor = $('#wrangulator fieldset').css('color');
  279. let boxshadow = $('#wrangulator fieldset').css('box-shadow');
  280.  
  281. $('#header').append(`<div style="background-color: ${bgcolor}; color: ${fontcolor};
  282. padding: 0.5em 0.5em 0.5em 1em;
  283. xmargin-top: -1em;
  284. box-shadow: ${boxshadow};
  285. text-align: center;
  286. font-size: 90%;">Fandom Resources: ${links.join(", ")}</div>`);
  287.  
  288. }
  289.  
  290. })(jQuery);