dndbeyond-json-exporter

Export your character sheets into JSON.

  1. // ==UserScript==
  2. // @name dndbeyond-json-exporter
  3. // @license GNU GPLv3
  4. // @namespace Violentmonkey Scripts
  5. // @match https://www.dndbeyond.com/characters*
  6. // @require http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
  7. // @grant GM_addStyle
  8. // @grant GM_download
  9. // @version 1.1
  10. // @author buhbbl
  11. // @description Export your character sheets into JSON.
  12. // ==/UserScript==
  13.  
  14. const API_URL = 'https://character-service.dndbeyond.com/character/v5/character/';
  15.  
  16. // =========== All Characters Page =============
  17. // div config
  18. const DDB_CHARACTER_LISTING = 'ddb-characters-listing';
  19. const CHARACTER_CARDS_CLASS = 'ddb-campaigns-character-card-footer-links';
  20.  
  21. // "a"/link config
  22. const LINK_TEXT = 'Export';
  23.  
  24. // ============= Character Sheet ===============
  25. // div config
  26. const CHARACTER_SHEET_CLASS = 'ddbc-character-tidbits__menu-callout';
  27.  
  28. // button config
  29. const SMALL_BUTTON_CLASS = 'character-button-small';
  30. const MEDIUM_BUTTON_CLASS = 'character-button-medium';
  31. const CLASSES_TO_REMOVE = ["character-button", "ddbc-button"];
  32. const BUTTON_CSS = "margin-left: 5px;"
  33.  
  34. // span config
  35. const BUTTON_CONTENT_SPAN_CLASS = 'ct-button__content';
  36. const EXPORT_BUTTON_TEXT = 'Export to JSON';
  37. const SPAN_CSS = "color: #fff;"
  38.  
  39. waitForKeyElements(`div.${CHARACTER_SHEET_CLASS},div.${DDB_CHARACTER_LISTING}`, Main);
  40.  
  41. function Main () {
  42. if (IsCharacterCardPage()) {
  43. CreateCardExportButtons();
  44. return
  45. }
  46.  
  47. if (IsCharacterSheet()) {
  48. EnlargeSheetManageButton();
  49. CreateSheetExportButton();
  50. return
  51. }
  52.  
  53. console.error("export-character-json: Can't find any element.");
  54. }
  55.  
  56. function IsCharacterCardPage () {
  57. return (document.getElementsByClassName(CHARACTER_CARDS_CLASS)[0] != null);
  58. }
  59.  
  60. function IsCharacterSheet () {
  61. return (document.getElementsByClassName(CHARACTER_SHEET_CLASS)[0] != null);
  62. }
  63.  
  64. function DownloadOnClick () {
  65. GM_download(API_URL + this.characterID, this.characterID + '.json')
  66. }
  67.  
  68. function AppendExportButton (character_card) {
  69. let characterID = character_card.children[0].href.split("/")[4];
  70.  
  71. let link = document.createElement("a");
  72. link.classList = character_card.firstElementChild.classList;
  73. link.innerHTML = LINK_TEXT;
  74. link.href = "javascript:;";
  75. link.onclick = DownloadOnClick;
  76. link.characterID = characterID;
  77.  
  78. character_card.insertBefore(link, character_card.children[2]);
  79. return true;
  80. }
  81.  
  82. function CreateCardExportButtons () {
  83. let containers = document.getElementsByClassName(CHARACTER_CARDS_CLASS);
  84.  
  85. for (let container of containers) {
  86. AppendExportButton(container);
  87. }
  88.  
  89. return true;
  90. }
  91.  
  92. function EnlargeSheetManageButton () {
  93. let container = document.getElementsByClassName(CHARACTER_SHEET_CLASS)[0];
  94. container.firstElementChild.classList.replace("character-button-small", "character-button-medium");
  95. return true;
  96. }
  97.  
  98. function CreateSheetExportButton () {
  99. let characterID = window.location.pathname.split("/")[2];
  100.  
  101. let container = document.getElementsByClassName(CHARACTER_SHEET_CLASS)[0];
  102.  
  103. let btn = document.createElement("button");
  104. btn.classList = container.firstElementChild.classList;
  105. btn.onclick = DownloadOnClick;
  106. btn.characterID = characterID;
  107. btn.style.cssText = BUTTON_CSS;
  108.  
  109. CLASSES_TO_REMOVE.forEach(element => {btn.classList.remove(element)});
  110.  
  111. let span = document.createElement("span");
  112. span.classList = [BUTTON_CONTENT_SPAN_CLASS];
  113. span.innerHTML = EXPORT_BUTTON_TEXT;
  114.  
  115. btn.appendChild(span);
  116. container.appendChild(btn);
  117. return true;
  118. }
  119.  
  120. // Author: BrockA (https://github.com/BrockA)
  121. // Source: https://gist.github.com/BrockA/2625891
  122. function waitForKeyElements (
  123. selectorTxt, /* Required: The jQuery selector string that
  124. specifies the desired element(s).
  125. */
  126. actionFunction, /* Required: The code to run when elements are
  127. found. It is passed a jNode to the matched
  128. element.
  129. */
  130. bWaitOnce, /* Optional: If false, will continue to scan for
  131. new elements even after the first match is
  132. found.
  133. */
  134. iframeSelector /* Optional: If set, identifies the iframe to
  135. search.
  136. */
  137. ) {
  138. var targetNodes, btargetsFound;
  139.  
  140. if (typeof iframeSelector == "undefined")
  141. targetNodes = $(selectorTxt);
  142. else
  143. targetNodes = $(iframeSelector).contents ()
  144. .find (selectorTxt);
  145.  
  146. if (targetNodes && targetNodes.length > 0) {
  147. btargetsFound = true;
  148. /*--- Found target node(s). Go through each and act if they
  149. are new.
  150. */
  151. targetNodes.each ( function () {
  152. var jThis = $(this);
  153. var alreadyFound = jThis.data ('alreadyFound') || false;
  154.  
  155. if (!alreadyFound) {
  156. //--- Call the payload function.
  157. var cancelFound = actionFunction (jThis);
  158. if (cancelFound)
  159. btargetsFound = false;
  160. else
  161. jThis.data ('alreadyFound', true);
  162. }
  163. } );
  164. }
  165. else {
  166. btargetsFound = false;
  167. }
  168.  
  169. //--- Get the timer-control variable for this selector.
  170. var controlObj = waitForKeyElements.controlObj || {};
  171. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  172. var timeControl = controlObj [controlKey];
  173.  
  174. //--- Now set or clear the timer as appropriate.
  175. if (btargetsFound && bWaitOnce && timeControl) {
  176. //--- The only condition where we need to clear the timer.
  177. clearInterval (timeControl);
  178. delete controlObj [controlKey]
  179. }
  180. else {
  181. //--- Set a timer, if needed.
  182. if ( ! timeControl) {
  183. timeControl = setInterval ( function () {
  184. waitForKeyElements ( selectorTxt,
  185. actionFunction,
  186. bWaitOnce,
  187. iframeSelector
  188. );
  189. },
  190. 300
  191. );
  192. controlObj [controlKey] = timeControl;
  193. }
  194. }
  195. waitForKeyElements.controlObj = controlObj;
  196. }