Kemono Discord Favourite Button

Add a favourite button to Discord server creator pages on Kemono

  1. // ==UserScript==
  2. // @name Kemono Discord Favourite Button
  3. // @namespace https://kemono.su/
  4. // @author Agent 9
  5. // @version 1.1
  6. // @license MIT
  7. // @description Add a favourite button to Discord server creator pages on Kemono
  8. // @match https://kemono.su/*
  9. // @grant window.onurlchange
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. // ————————————————————————
  15. // SPA Navigation Listener
  16. // ————————————————————————
  17.  
  18. window.addEventListener('urlchange', () => {
  19. waitForElement('#main ul', (ul) => {
  20. initFavoriteButton();
  21. });
  22. console.log('url changed');
  23. const targetNode = document.querySelector("#main")
  24. console.log(targetNode);
  25. const config = { attributes: true, childList: true, subtree: true };
  26. const callback = (mutationList, observer) => {
  27. for (const mutation of mutationList) {
  28. if (mutation.type === "childList") {
  29. initFavoriteButton();
  30. } else if (mutation.type === "attributes") {
  31. initFavoriteButton();
  32. }
  33. }
  34. };
  35. const observer = new MutationObserver(callback);
  36. observer.observe(targetNode, config);
  37. });
  38. window.addEventListener('load', () => {
  39. waitForElement('#main ul', (ul) => {
  40. initFavoriteButton();
  41. });
  42. console.log('load');
  43. });
  44.  
  45. // ————————————————————————
  46. // Utilities
  47. // ————————————————————————
  48. function waitForElement(selector, callback) {
  49. const el = document.querySelector(selector);
  50. if (el) return callback(el);
  51. setTimeout(() => waitForElement(selector, callback), 100);
  52. }
  53.  
  54. function getServerId(url) {
  55. const parts = url.split("/"); // splits into parts by "/"
  56. const serverIndex = parts.indexOf("server"); // find "server"
  57. return serverIndex !== -1 ? parts[serverIndex + 1].trim() : null;
  58. }
  59.  
  60. function createButton(service,profileID){
  61. let btn = document.createElement('button');
  62. btn.id = 'favorite-button-template';
  63. btn.className = 'user-header__favourite';
  64. btn.type = 'button';
  65. btn.innerHTML = `<span class="user-header__fav-icon">☆</span>
  66. <span class="user-header__fav-text">Favorite</span>`;
  67.  
  68. // Set initial state based on current favourites
  69. fetch('/api/v1/account/favorites', {
  70. method: 'GET',
  71. credentials: 'include'
  72. })
  73. .then(function(response){
  74. return response.json();
  75. })
  76. .then(function(data){
  77. const isFav = data.some(item => item.service == service && item.id == profileID);
  78. console.log(isFav);
  79. if(isFav==true){
  80. btn.className = 'user-header__favourite user-header__favourite--unfav ';
  81. btn.innerHTML =`<span class="user-header__fav-icon">★</span>
  82. <span class="user-header__fav-text">Unfavorite</span>`;
  83. }
  84. })
  85. .catch(error => console.error('Error:', error));
  86.  
  87. return btn;
  88. }
  89.  
  90.  
  91. // ————————————————————————
  92. // Main Injector / Syncer
  93. // ————————————————————————
  94. function initFavoriteButton() {
  95. // Only target Discord creator pages
  96. if (!/\/discord\//.test(location.pathname)){
  97. console.log('not a discord server');
  98. return;
  99. }
  100.  
  101. // Prevent duplicate injection
  102. if (document.getElementById('creator-actions')){
  103. console.log('creator-actions already injected');
  104. return;
  105. }
  106. // Create the favorite button
  107. let service = 'discord';
  108. let profileID = getServerId(location.pathname);
  109. let btn = createButton(service,profileID);
  110.  
  111. // Toggle favourite on click
  112. btn.addEventListener('click', () => {
  113. const isNowFav = btn.classList.toggle('user-header__favourite--unfav');
  114. if(isNowFav){
  115. btn.innerHTML =`<span class="user-header__fav-icon">★</span>
  116. <span class="user-header__fav-text">Unfavorite</span>`;
  117. }
  118. else{
  119. btn.innerHTML = `<span class="user-header__fav-icon">☆</span>
  120. <span class="user-header__fav-text">Favorite</span>`;
  121. }
  122. const method = isNowFav ? 'POST' : 'DELETE';
  123.  
  124. fetch(`/api/v1/favorites/creator/${service}/${profileID}`, {
  125. method,
  126. credentials: 'include',
  127. }).catch(console.error);
  128. });
  129.  
  130. // Inject into page when element is ready
  131. waitForElement('#main ul', (ul) => {
  132. const container = document.createElement('div');
  133. container.id = 'creator-actions';
  134. container.appendChild(btn);
  135. ul.prepend(container);
  136. });
  137. }
  138. })();