AO3: [Wrangling] Fandom Resources Quicklinks

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

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

  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 0.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 subfandoms point to the AMT which has some resources configured
  28. resources = {
  29. "Wiedźmin | The Witcher - All Media Types": [ ["wikia", "https://witcher.fandom.com/wiki/Witcher_Wiki"],
  30. ["IMDB Cast", "https://www.imdb.com/title/tt5180504/fullcredits/"] ],
  31. "Wiedźmin | The Witcher (Video Game)": "Wiedźmin | The Witcher - All Media Types",
  32. "Wiedźmin | The Witcher Series - Andrzej Sapkowski": "Wiedźmin | The Witcher - All Media Types",
  33. "The Witcher (TV)": "Wiedźmin | The Witcher - All Media Types"
  34. };
  35. */
  36. let resources = loadConfig();
  37.  
  38. // --- CONFIGURATION DIALOG HANDLING -------------------------------------------------------------------------------
  39.  
  40. createDialog();
  41.  
  42. function createDialog() {
  43.  
  44. // if the background is dark, use the dark UI theme to match
  45. let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "dark-hive" : "base";
  46.  
  47. // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling
  48. $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
  49. .append(`<style tyle="text/css">${dlg}, .ui-dialog .ui-dialog-buttonpane button {font-size: revert; line-height: 1.286;}
  50. ${dlg} form {box-shadow: revert; cursor:auto;}
  51. ${dlg} fieldset {background: revert; box-shadow: revert;}
  52. ${dlg} fieldset p { padding-left: 0; padding-right: 0; }
  53. ${dlg} legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
  54. ${dlg} fieldset input::placeholder { font-style: italic; opacity: 0.2; }
  55. ${dlg} fieldset input[type="text"] { padding: 0.2em 0.5em; width: 20em; }
  56. ${dlg} fieldset input[type="text"][id^="display"] { width: 10em; }
  57. ${dlg} fieldset p.indented { padding: 0.2em 0 0.2em 2em; }
  58. ${dlg} fieldset div.fandom, ${dlg} fieldset div.fandom-new { margin: 1em 0 0 0; }
  59. ${dlg} fieldset button { xfont-size: 80%; margin: 0.1em; }
  60. ${dlg} fieldset ul.autocomplete { display: inline-block; }
  61. ${dlg} ul.autocomplete li { margin: 0; }
  62. </style>`);
  63.  
  64. // wrapper div for the dialog
  65. $("#main").append(`<div id="${cfg}"></div>`);
  66.  
  67. // these create the necessary blank HTML fields that get dynamically added on button-clicks
  68. let newresource = templateResource();
  69. let newlinked = templateLink();
  70. let newfandom = templateFandom();
  71.  
  72. let prevStoredHTML = "";
  73. for (let [f, r] of Object.entries(resources)) {
  74. prevStoredHTML += templateFandom(f, r);
  75. }
  76.  
  77. $(dlg).html(`<form>
  78. <fieldset><legend>Instructions</legend>
  79. <div class="userstuff">
  80. <p style="margin-top: 0;">Select the "Add Fandom" button for input fields to add another fandom to the list.</p>
  81. <p>The Fandom fields offer autocomplete, so you can easily choose the canonical fandom tag.</p>
  82. <p style="margin-bottom: 0;">For each fandom, you can choose if it</p>
  83. <ul style="margin-top: 0;">
  84. <li>gets its own set of resources (display text + URL)</li>
  85. <li>should be linked to another fandom (e.g. its metatag) and show those same resources</li>
  86. </ul></div>
  87. </fieldset>
  88. <fieldset style="max-height: 40em; overflow: auto;"><legend>Fandom Resources</legend>
  89. ${prevStoredHTML}
  90. <div class="fandom-new"><button name="add-fandom" type="button"><i class="fa fa-plus-square" aria-hidden="true"></i> Add Fandom</button></div>
  91. </fieldset>
  92. </form>`);
  93.  
  94. // optimizing the size of the GUI in case it's a mobile device
  95. let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px)
  96. if (dialogwidth < 1000) $("head").append(`<style tyle="text/css"> ${dlg} label { display: none; } </style>`); // saving some space
  97. dialogwidth = dialogwidth > 500 ? dialogwidth * 0.7 : dialogwidth * 0.9;
  98.  
  99. $(dlg).dialog({
  100. appendTo: "#main",
  101. modal: true,
  102. title: 'Quicklinks to Fandom Resources Config',
  103. draggable: true,
  104. resizable: false,
  105. autoOpen: false,
  106. width: dialogwidth,
  107. position: {my:"center", at: "center top"},
  108. buttons: {
  109. Reset: deleteConfig,
  110. Save: storeConfig,
  111. Cancel: function() { $( dlg ).dialog( "close" ); }
  112. }
  113. });
  114.  
  115. // if no other script has created it yet, write out a "Userscripts" option to the main navigation
  116. if ($('#scriptconfig').length == 0) {
  117. $('#header ul.primary.navigation li.dropdown').last()
  118. .after(`<li class="dropdown" id="scriptconfig">
  119. <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
  120. <ul class="menu dropdown-menu"></ul></li>`);
  121. }
  122. // then add this script's config option to navigation dropdown
  123. $('#scriptconfig .dropdown-menu').append(`<li><a href="javascript:void(0);" id="opencfg_${cfg}">Fandom Resources</a></li>`);
  124.  
  125. // on click, open the configuration dialog
  126. $("#opencfg_"+cfg).on("click", function(e) {
  127. $( dlg ).dialog('open');
  128. });
  129.  
  130. // delegated event handler for reactive GUI: adding/removing fandoms/resources/links
  131. $(dlg).on("click", "fieldset button", function(e) {
  132. e.preventDefault();
  133.  
  134. let parent = $(e.target).parent();
  135.  
  136. // depending on the button that was clicked, we add/remove different rows of data or hide buttons
  137. switch (e.target.name) {
  138. case "set-link":
  139. $(parent).before(newlinked).find('[name="set-link"], [name="add-resource"]').hide();
  140. break;
  141. case "add-resource":
  142. $(parent).before(newresource).find('[name="set-link"]').hide();
  143. break;
  144. case "delete-resource":
  145. if ($(parent).parent().find('p.resource').length === 1) $(parent).parent().find('[name="set-link"], [name="add-resource"]').show();
  146. $(parent).remove();
  147. break;
  148. case "delete-link":
  149. $(parent).parent().find('[name="set-link"], [name="add-resource"]').show();
  150. $(parent).remove();
  151. break;
  152. case "delete-fandom":
  153. $(parent).parent().remove();
  154. break;
  155. case "add-fandom":
  156. $(parent).before(newfandom);
  157. break;
  158. }
  159. });
  160. }
  161.  
  162. // --- HELPER FUNCTIONS TO CREATE GUI HTML -------------------------------------------------------------------------------
  163.  
  164. function templateFandom(f, r) {
  165. f = f ?? ""; // avoids that we print "undefined" if this is called for the blank-for-adding-fandom instance
  166. let resourcesHTML = ""; // holds HTML of the resource/fandom link configuration that was stored for the given fandom
  167. let visibleButtons = [true, true]; // initialize visible buttons when stored fandom config is shown in GUI
  168.  
  169. // when a bunch of resource links were configured for this fandom, we build those as HTML & hide the fandom-link button
  170. if (r instanceof Array) {
  171. for (let entry of r) { resourcesHTML += templateResource(entry); }
  172. visibleButtons[1] = false;
  173. }
  174. // when fandom was linked to another, we build that HTML & hide both resource and fandom-link button
  175. else if (typeof r === "string") {
  176. resourcesHTML = templateLink(r);
  177. visibleButtons = [false, false];
  178. }
  179.  
  180. return `
  181. <div class="fandom">
  182. <label for="fandom[]">Fandom:</label>
  183. <input type="text" id="fandom[]" name="fandom[]" class="fandom autocomplete" data-autocomplete-method="/autocomplete/fandom"
  184. data-autocomplete-hint-text="Start typing for Fandom suggestions!" data-autocomplete-no-results-text="(No suggestions found)"
  185. data-autocomplete-min-chars="1" data-autocomplete-searching-text="Searching..." data-autocomplete-token-limit="1" value="${f}"/>
  186. ${resourcesHTML}
  187. <p class="indented add-new">
  188. <button name="add-resource" type="button" ${ visibleButtons[0] ? '' : 'style="display: none;"' }><i class="fa fa-plus-square-o" aria-hidden="true"></i> Add Resource</button>
  189. <button name="set-link" type="button" ${ visibleButtons[1] ? '' : 'style="display: none;"' }><i class="fa fa-hand-o-right" aria-hidden="true"></i> Link to Another Fandom</button>
  190. <button name="delete-fandom" type="button"><i class="fa fa-trash" aria-hidden="true"></i> Delete Fandom Config</button>
  191. </p>
  192. </div>`;
  193. }
  194.  
  195. function templateResource(r = ["", ""]) {
  196. return `
  197. <p class="indented resource">
  198. <i class="fa fa-external-link" aria-hidden="true"></i>
  199. <label for="display[]">Resource:</label> <input type="text" id="display[]" name="display[]" placeholder="e.g. IMDB" value="${r[0]}" />
  200. <label for="url[]">URL:</label> <input type="text" id="url[]" name="url[]" placeholder="e.g. http://www.imdb.com" value="${r[1]}" />
  201. <button name="delete-resource" type="button"><i class="fa fa-minus-square-o" aria-hidden="true"></i> Remove Resource</button>
  202. </p>`;
  203. }
  204.  
  205. function templateLink(l="") {
  206. return `
  207. <p class="indented linked">
  208. <i class="fa fa-hand-o-right" aria-hidden="true"></i> <label for="linked[]">uses same resources as fandom:</label>
  209. <input type="text" id="linked[]" name="linked[]" class="fandom autocomplete" data-autocomplete-method="/autocomplete/fandom"
  210. data-autocomplete-hint-text="Start typing for Fandom suggestions!" data-autocomplete-no-results-text="(No suggestions found)"
  211. data-autocomplete-min-chars="1" data-autocomplete-searching-text="Searching..." data-autocomplete-token-limit="1" value="${l}"/>
  212. <button name="delete-link" type="button"><i class="fa fa-chain-broken" aria-hidden="true"></i> Unlink Fandoms</button>
  213. </p>`;
  214. }
  215.  
  216. // --- LOCALSTORAGE MANIPULATION -------------------------------------------------------------------------------
  217.  
  218. function deleteConfig() {
  219. if (confirm('Are you sure you want to delete all Fandom Resource quicklinks?')) {
  220. localStorage.removeItem(cfg);
  221. $(dlg).dialog('close');
  222. }
  223. }
  224.  
  225. function storeConfig() {
  226. // object to start collecting our storage data
  227. let fandom_resources = {};
  228.  
  229. // grab all the elements and fields
  230. $(dlg).find('div.fandom').each(function(ix) {
  231. let fandom = $(this).find('> ul.autocomplete li.added.tag');
  232.  
  233. if ($(fandom).length === 1) {
  234. fandom = $(fandom).contents().eq(0).text().trim();
  235.  
  236. let linkedfandom = $(this).find('p.linked > ul.autocomplete li.added.tag');
  237. let resources = $(this).find('p.resource');
  238.  
  239. if ($(linkedfandom).length === 1 && $(resources).length > 0) {
  240. console.log('resources for fandom '+fandom+' could not be stored, both link and resources given - only one of these is allowed');
  241. }
  242. else if ($(resources).length > 0) {
  243. let resource_list = []; // to keep this simple, we'll use an Array because JSON.stringify(Map) ends up with an empty {}
  244. $(resources).each(function() {
  245. let display = $(this).find('input[id="display[]"]').prop('value') || "";
  246. let url = $(this).find('input[id="url[]"]').prop('value') || "";
  247. resource_list.push([display, url]);
  248. });
  249.  
  250. fandom_resources[fandom] = resource_list;
  251. }
  252. else if ($(linkedfandom).length === 1) {
  253. linkedfandom = $(linkedfandom).contents().eq(0).text().trim();
  254. fandom_resources[fandom] = linkedfandom;
  255. }
  256. else console.log('resources for fandom '+fandom+' could not be stored, no link nor resources given');
  257. }
  258. else console.log('resources for entry #'+ix+' could not be stored, no fandom given');
  259. });
  260.  
  261. // by the end of this, we've filled up fandom_resources with all data and are ready to store
  262. localStorage.setItem(cfg, JSON.stringify(fandom_resources));
  263. $(dlg).dialog('close');
  264. }
  265.  
  266. function loadConfig() {
  267. return JSON.parse(localStorage.getItem(cfg) ?? "{}");
  268. }
  269.  
  270. // --- WRITING THE TOP BAR WITH THE WRANGLING RESOURCE LINKS -------------------------------------------------------------------------------
  271.  
  272. // if this isn't a fandom bin, quit because we don't know the fandom to show resources for
  273. // also quit if there are no tags to display: mostly because we rely on #wrangulator to provide the styling
  274. if ($('#inner').find('ul.navigation.actions').eq(1).find('li').length != 5 || $('#wrangulator').length < 1) return;
  275.  
  276. // grab the currently viewed fandom name
  277. let fandom = $('#main > .heading a.tag').text();
  278.  
  279. // if this fandom points to another fandom (or just some nickname), we'll continue looking for links under that other fandom name
  280. if (typeof resources[fandom] === "string") fandom = resources[fandom];
  281.  
  282. // if there are links configured, we'll display them. or it's an empty bar.
  283. if (resources[fandom] instanceof Array) {
  284. let links = [];
  285. resources[fandom].forEach((val) => links.push(`<a href="${val[1]}">${val[0]} <i class="fa fa-external-link" aria-hidden="true"></i></a>`));
  286.  
  287. let bgcolor = $('#wrangulator fieldset').css('background-color');
  288. let fontcolor = $('#wrangulator fieldset').css('color');
  289. let boxshadow = $('#wrangulator fieldset').css('box-shadow');
  290.  
  291. $('#header').append(`<div style="background-color: ${bgcolor}; color: ${fontcolor};
  292. padding: 0.5em 0.5em 0.5em 1em;
  293. xmargin-top: -1em;
  294. box-shadow: ${boxshadow};
  295. text-align: center;
  296. font-size: 90%;">Fandom Resources: ${links.join(", ")}</div>`);
  297. }
  298. })(jQuery);