Kemono.Party Discord Favourites

Allows favouriting of Discord servers

  1. // ==UserScript==
  2. // @name Kemono.Party Discord Favourites
  3. // @namespace https://MeusArtis.ca
  4. // @version 2.0.1
  5. // @author Meus Artis, gwhizzy
  6. // @description Allows favouriting of Discord servers
  7. // @icon https://www.google.com/s2/favicons?domain=kemono.party
  8. // @supportURL https://t.me/kemonoparty
  9. // @include /^https:\/\/kemono\.(party|su)\/discord\/server\/.*$/
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
  11. // @license CC BY-NC-SA 4.0
  12. // ==/UserScript==
  13. const strs =
  14. {
  15. loading: "⋯ Loading",
  16. fav: "☆ Favorite",
  17. unfav: "★ Unfavorite",
  18. error: "! Error"
  19. };
  20. const classname_fav = "__dcfav_tofav";
  21. const classname_unfav = "__dcfav_tounfav";
  22. const STATE_UNSET = -1;
  23. const STATE_LOAD = 0;
  24. const STATE_FAV = 1;
  25. const STATE_UNFAV = 2;
  26. const STATE_ERROR = 3;
  27. var curr_favState = STATE_UNSET;
  28. var cache_canread = true;
  29. var cache_lastState = "[]";
  30. function get_server_id()
  31. {
  32. let id = location.href.split("/").pop();
  33. return id ? id : "";
  34. }
  35. function get_parent_el()
  36. {
  37. return document.getElementById("channels");
  38. }
  39. function styles_inject()
  40. {
  41. const style = document.createElement("style");
  42. style.innerText = `.__dcfav_base{box-sizing:border-box;font-weight:700;border:transparent;user-select:none;font-family:Helvetica,sans-serif;color:#888}.__dcfav_tofav{color:#fff}.__dcfav_tounfav{color:#ffdd1a}`;
  43. document.head.appendChild(style);
  44. }
  45. function button_inject(parent)
  46. {
  47. let o = document.createElement("div");
  48. let e = document.createElement("div");
  49. e.classList.add("__dcfav_base");
  50. o.style.textAlign = "center";
  51. parent.appendChild(o).appendChild(e);
  52. }
  53. function button_change_state(e, favState)
  54. {
  55. curr_favState = favState;
  56. let toFav = true;
  57. e.classList.remove(classname_fav);
  58. e.classList.remove(classname_unfav);
  59. switch (favState)
  60. {
  61. case STATE_LOAD:
  62. {
  63. e.innerText = strs.loading;
  64. e.onclick = undefined;
  65. e.style.cursor = "default";
  66. return; // nothing else to do, get out
  67. }
  68. case STATE_ERROR:
  69. {
  70. e.innerText = strs.error;
  71. e.style.cursor = "pointer";
  72. break; // nothing else to do, get out
  73. }
  74. case STATE_FAV:
  75. {
  76. e.innerText = strs.fav;
  77. e.classList.add(classname_fav);
  78. e.style.cursor = "pointer";
  79. break;
  80. }
  81. case STATE_UNFAV:
  82. {
  83. e.innerText = strs.unfav;
  84. e.classList.add(classname_unfav);
  85. e.style.cursor = "pointer";
  86. toFav = false;
  87. break;
  88. }
  89. default: return;
  90. }
  91. e.onclick = function ()
  92. {
  93. cache_canread = false;
  94. button_set_state(STATE_LOAD);
  95. fetch("https://kemono.party/favorites/artist/discord/" + get_server_id(), { method: toFav ? "POST" : "DELETE" })
  96. .catch(() => {})
  97. .then(_ => button_update());
  98. }
  99. }
  100. async function get_next_fav_state()
  101. {
  102. let id = get_server_id();
  103. try
  104. {
  105. let json = await (await fetch("https://kemono.party/api/v1/account/favorites?type=artist")).json();
  106. // Update localstorage cache
  107. let cache_newState = JSON.stringify(json);
  108. localStorage.setItem("favs", cache_newState);
  109. cache_lastState = cache_newState;
  110. // Iterate over results to find a match
  111. for (let o of json)
  112. if (o?.id === id)
  113. // Favourite, show button to unfavourite
  114. return STATE_UNFAV;
  115. }
  116. catch (e)
  117. {
  118. console.error(e);
  119. return STATE_ERROR;
  120. }
  121. // Otherwise, show button to favourite
  122. return STATE_FAV;
  123. }
  124. function button_set_state(favState)
  125. {
  126. let es = document.getElementsByClassName("__dcfav_base");
  127. if (!es.length)
  128. // Button not found, ignore
  129. return;
  130. button_change_state(es[0], favState);
  131. }
  132. async function button_update(showLoading=true)
  133. {
  134. if (showLoading)
  135. // Set default state while querying server for favourite status
  136. button_set_state(STATE_LOAD);
  137. let favState = await get_next_fav_state();
  138. button_set_state(favState);
  139. cache_canread = true;
  140. }
  141. function cache_init()
  142. {
  143. // Should be done by the site anyway
  144. if (!("favs" in localStorage))
  145. localStorage.setItem("favs", "[]");
  146. cache_lastState = localStorage.getItem("favs");
  147. if (!cache_lastState)
  148. cache_lastState = "[]";
  149. }
  150. function cache_update_from(force=false)
  151. {
  152. if (cache_canread)
  153. {
  154. let id = get_server_id();
  155. // Update localstorage cache
  156. let favs = localStorage.getItem("favs");
  157. if (!favs)
  158. favs = "[]";
  159. if (force || favs !== cache_lastState)
  160. {
  161. // Detected an updated cache, load into dom
  162. let json = JSON.parse(favs);
  163. let showFav = true;
  164. // Iterate over results to find a match
  165. for (let o of json)
  166. if (o?.id === id)
  167. // Favourite, show button to unfavourite
  168. showFav = false;
  169. // Change state if not set already
  170. {
  171. if (showFav && curr_favState !== STATE_FAV)
  172. button_set_state(STATE_FAV);
  173. else if (!showFav && curr_favState !== STATE_UNFAV)
  174. button_set_state(STATE_UNFAV);
  175. }
  176. }
  177. }
  178. // Run on (safe) interval
  179. setTimeout(cache_update_from, 1000);
  180. }
  181. function main()
  182. {
  183. let parent = get_parent_el();
  184. if (!parent)
  185. // Not a discord server
  186. return;
  187. cache_init();
  188. styles_inject();
  189. button_inject(parent);
  190. cache_update_from(true); // dispatch interval
  191. button_update(false); // dispatch async
  192. }
  193. main();