AniList Shortcuts

Add multiples shortcuts + custom ones

  1. // ==UserScript==
  2. // @name AniList Shortcuts
  3. // @version 1.0
  4. // @description Add multiples shortcuts + custom ones
  5. // @author Mio.
  6. // @namespace https://github.com/dear-clouds/mio-userscripts
  7. // @supportURL https://github.com/dear-clouds/mio-userscripts/issues
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=anilist.co
  9. // @license GPL-3.0
  10. // @match *://*.anilist.co/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Function to inject Font Awesome CSS
  18. function injectFontAwesome() {
  19. const faLink = document.createElement('link');
  20. faLink.rel = 'stylesheet';
  21. faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
  22. faLink.integrity = 'sha512-pap5K1fL5c4sLcXmpopbPWha8z36H1EJGgUK6YyE1Wfo2jydN12wPuABanVbBv8d5kZdO8+8PpJ1f8kz0gJ0Mg==';
  23. faLink.crossOrigin = 'anonymous';
  24. faLink.referrerPolicy = 'no-referrer';
  25. document.head.appendChild(faLink);
  26. }
  27.  
  28. injectFontAwesome();
  29.  
  30. // Utility function to wait for an element to appear in the DOM
  31. function waitForElement(selector, timeout = 10000) {
  32. return new Promise((resolve, reject) => {
  33. const intervalTime = 100;
  34. let timeElapsed = 0;
  35.  
  36. const interval = setInterval(() => {
  37. const element = document.querySelector(selector);
  38. if (element) {
  39. clearInterval(interval);
  40. resolve(element);
  41. }
  42. timeElapsed += intervalTime;
  43. if (timeElapsed >= timeout) {
  44. clearInterval(interval);
  45. reject(`Element ${selector} not found within ${timeout}ms`);
  46. }
  47. }, intervalTime);
  48. });
  49. }
  50.  
  51. // Function to add a link with Font Awesome icon
  52. function addLinkWithIcon(element, url, linkText, iconName) {
  53. const link = document.createElement('a');
  54. link.href = url;
  55. link.target = '_blank';
  56. link.rel = 'noopener noreferrer';
  57. link.style.textDecoration = 'none';
  58. link.style.color = 'inherit';
  59. link.style.display = 'flex';
  60. link.style.alignItems = 'center';
  61. link.style.marginLeft = '10px';
  62.  
  63. const icon = document.createElement('i');
  64. icon.className = `fa fa-${iconName}`;
  65. icon.style.marginRight = '5px';
  66.  
  67. link.appendChild(icon);
  68. link.appendChild(document.createTextNode(linkText));
  69. element.appendChild(link);
  70. }
  71.  
  72. // Function to create a sticky box on forum comments
  73. function createStickyBoxLink() {
  74. // Prevent multiple sticky boxes
  75. if (document.querySelector('.sticky-box')) return;
  76.  
  77. const stickyBox = document.createElement('div');
  78. stickyBox.classList.add('sticky-box');
  79. stickyBox.style.position = 'fixed';
  80. stickyBox.style.right = '10px';
  81. stickyBox.style.top = '200px';
  82. stickyBox.style.width = '200px';
  83. stickyBox.style.padding = '10px';
  84. stickyBox.style.backgroundColor = 'rgb(var(--color-foreground))';
  85. stickyBox.style.borderRadius = '4px';
  86. stickyBox.style.transition = 'height 0.5s, opacity 0.5s';
  87. stickyBox.style.overflow = 'hidden';
  88. stickyBox.style.zIndex = '1000';
  89.  
  90. const header = document.createElement('h3');
  91. header.innerText = 'Shortcuts';
  92. header.style.fontSize = 'medium';
  93. header.style.marginTop = '5';
  94. stickyBox.appendChild(header);
  95.  
  96. const linksContainer = document.createElement('div');
  97. stickyBox.appendChild(linksContainer);
  98.  
  99. const addIcon = document.createElement('span');
  100. addIcon.innerText = '+';
  101. addIcon.style.position = 'absolute';
  102. addIcon.style.top = '5px';
  103. addIcon.style.right = '5px';
  104. addIcon.style.cursor = 'pointer';
  105. addIcon.style.fontWeight = 'bold';
  106. addIcon.onclick = function () {
  107. const isHidden = userInput.style.display === 'none';
  108. userInput.style.display = isHidden ? 'block' : 'none';
  109. shortcutNameInput.style.display = isHidden ? 'block' : 'none';
  110. validateButton.style.display = isHidden ? 'block' : 'none';
  111. };
  112. stickyBox.appendChild(addIcon);
  113.  
  114. const toggleVisibilityIcon = document.createElement('i');
  115. toggleVisibilityIcon.className = 'fa fa-eye';
  116. toggleVisibilityIcon.style.cursor = 'pointer';
  117. toggleVisibilityIcon.style.position = 'absolute';
  118. toggleVisibilityIcon.style.top = '5px';
  119. toggleVisibilityIcon.style.left = '5px';
  120. toggleVisibilityIcon.style.fontSize = '14px';
  121. toggleVisibilityIcon.onclick = function () {
  122. if (stickyBox.style.height !== '25px') {
  123. stickyBox.style.width = '25px';
  124. stickyBox.style.height = '25px';
  125. linksContainer.style.display = 'none';
  126. header.style.display = 'none';
  127. addIcon.style.display = 'none';
  128. toggleVisibilityIcon.className = 'fa fa-eye-slash';
  129. } else {
  130. stickyBox.style.width = '200px';
  131. stickyBox.style.height = 'auto';
  132. linksContainer.style.display = 'block';
  133. header.style.display = 'block';
  134. addIcon.style.display = 'block';
  135. toggleVisibilityIcon.className = 'fa fa-eye';
  136. }
  137. };
  138. stickyBox.appendChild(toggleVisibilityIcon);
  139.  
  140. function appendLinkToContainer(linkName, linkURL) {
  141. const linkElement = document.createElement('a');
  142. linkElement.href = linkURL;
  143. linkElement.innerText = linkName;
  144. linkElement.style.fontSize = 'smaller';
  145. linkElement.target = '_blank';
  146. linkElement.style.display = 'flex';
  147. linkElement.style.alignItems = 'center';
  148. linkElement.style.marginBottom = '5px';
  149. linkElement.style.color = 'var(--color-blue)';
  150. linkElement.style.textDecoration = 'none';
  151.  
  152. linkElement.addEventListener('click', (e) => {
  153. e.preventDefault();
  154. window.open(linkURL, '_blank');
  155. });
  156.  
  157. const favicon = document.createElement('img');
  158. try {
  159. const urlObj = new URL(linkURL);
  160. favicon.src = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}`;
  161. } catch {
  162. favicon.src = '';
  163. }
  164. favicon.style.marginRight = '5px';
  165. favicon.style.width = '16px';
  166. favicon.style.height = '16px';
  167. linkElement.prepend(favicon);
  168.  
  169. const deleteIcon = document.createElement('span');
  170. deleteIcon.innerText = ' ×';
  171. deleteIcon.style.color = 'rgb(var(--color-blue))';
  172. deleteIcon.style.cursor = 'pointer';
  173. deleteIcon.style.marginLeft = 'auto';
  174. deleteIcon.onclick = function (event) {
  175. event.stopPropagation();
  176. linksContainer.removeChild(linkElement);
  177. const savedLinks = JSON.parse(localStorage.getItem('MioAniListShortcuts') || '[]');
  178. const updatedLinks = savedLinks.filter(l => l.url !== linkURL);
  179. localStorage.setItem('MioAniListShortcuts', JSON.stringify(updatedLinks));
  180. };
  181. linkElement.appendChild(deleteIcon);
  182.  
  183. linksContainer.appendChild(linkElement);
  184. }
  185.  
  186. const userInput = document.createElement('input');
  187. userInput.type = 'text';
  188. userInput.placeholder = 'Enter your link';
  189. userInput.style.display = 'none';
  190. userInput.style.backgroundColor = 'rgb(var(--color-background))';
  191. userInput.style.color = 'rgb(var(--color-blue))';
  192. userInput.style.border = '1px solid var(--color-border)';
  193. userInput.style.borderRadius = '3px';
  194. userInput.style.padding = '5px';
  195. userInput.style.fontSize = 'smaller';
  196. userInput.style.marginTop = '5px';
  197. stickyBox.appendChild(userInput);
  198.  
  199. const shortcutNameInput = document.createElement('input');
  200. shortcutNameInput.type = 'text';
  201. shortcutNameInput.placeholder = 'Name of the shortcut';
  202. shortcutNameInput.style.display = 'none';
  203. shortcutNameInput.style.backgroundColor = 'rgb(var(--color-background))';
  204. shortcutNameInput.style.color = 'rgb(var(--color-blue))';
  205. shortcutNameInput.style.border = '1px solid var(--color-border)';
  206. shortcutNameInput.style.borderRadius = '3px';
  207. shortcutNameInput.style.padding = '5px';
  208. shortcutNameInput.style.fontSize = 'smaller';
  209. shortcutNameInput.style.marginTop = '5px';
  210. stickyBox.appendChild(shortcutNameInput);
  211.  
  212. const validateButton = document.createElement('button');
  213. validateButton.innerText = 'Add';
  214. validateButton.style.display = 'none';
  215. validateButton.style.backgroundColor = 'var(--color-button)';
  216. validateButton.style.color = 'var(--color-button-text)';
  217. validateButton.style.border = 'none';
  218. validateButton.style.borderRadius = '3px';
  219. validateButton.style.padding = '5px 10px';
  220. validateButton.style.fontSize = 'smaller';
  221. validateButton.style.marginTop = '5px';
  222. validateButton.style.cursor = 'pointer';
  223. validateButton.onclick = function () {
  224. const link = userInput.value.trim();
  225. const name = shortcutNameInput.value.trim();
  226. if (link && name) {
  227. const savedLinks = JSON.parse(localStorage.getItem('MioAniListShortcuts') || '[]');
  228. // Avoid duplicates
  229. if (!savedLinks.some(l => l.url === link)) {
  230. savedLinks.push({ name, url: link });
  231. localStorage.setItem('MioAniListShortcuts', JSON.stringify(savedLinks));
  232.  
  233. appendLinkToContainer(name, link);
  234.  
  235. userInput.value = '';
  236. shortcutNameInput.value = '';
  237. userInput.style.display = 'none';
  238. shortcutNameInput.style.display = 'none';
  239. validateButton.style.display = 'none';
  240. } else {
  241. alert('This link already exists in your shortcuts.');
  242. }
  243. } else {
  244. alert('Please enter both name and URL.');
  245. }
  246. };
  247. stickyBox.appendChild(validateButton);
  248.  
  249. // Load saved links
  250. const savedLinks = JSON.parse(localStorage.getItem('MioAniListShortcuts') || '[]');
  251. for (const linkObj of savedLinks) {
  252. appendLinkToContainer(linkObj.name, linkObj.url);
  253. }
  254.  
  255. document.body.appendChild(stickyBox);
  256. }
  257.  
  258. // Function to add AniCalendar by KangieDanie link in Activity History
  259. // https://anilist.co/forum/thread/63096
  260. function addAniCalendarLink() {
  261. // Prevent adding multiple links
  262. if (document.querySelector('.ani-calendar-link')) return;
  263.  
  264. let attempts = 0;
  265. const maxAttempts = 10; // 20 seconds max
  266. const interval = setInterval(() => {
  267. const headers = document.querySelectorAll('h2.section-header');
  268. let activityHistoryHeader = null;
  269.  
  270. headers.forEach(header => {
  271. if (header.textContent.trim() === 'Activity History') {
  272. activityHistoryHeader = header;
  273. }
  274. });
  275.  
  276. if (activityHistoryHeader) {
  277. // Prevent adding multiple links
  278. if (activityHistoryHeader.querySelector('.ani-calendar-link')) {
  279. clearInterval(interval);
  280. return;
  281. }
  282.  
  283. const aniCalendarContainer = document.createElement('span');
  284. aniCalendarContainer.classList.add('ani-calendar-link');
  285. aniCalendarContainer.style.float = 'right';
  286. aniCalendarContainer.style.display = 'flex';
  287. aniCalendarContainer.style.alignItems = 'center';
  288.  
  289. const aniCalendarLink = document.createElement('a');
  290. aniCalendarLink.href = 'https://ani-calendar.vercel.app/';
  291. aniCalendarLink.target = '_blank';
  292. aniCalendarLink.rel = 'noopener noreferrer';
  293. aniCalendarLink.textContent = 'AniCalendar';
  294. aniCalendarLink.style.fontSize = 'smaller';
  295. aniCalendarLink.style.marginLeft = '10px';
  296. aniCalendarLink.style.color = 'var(--color-blue)';
  297. aniCalendarLink.style.display = 'flex';
  298. aniCalendarLink.style.alignItems = 'center';
  299. aniCalendarLink.style.textDecoration = 'none';
  300.  
  301. const calendarIcon = document.createElement('i');
  302. calendarIcon.className = 'fa fa-calendar';
  303. calendarIcon.style.marginRight = '5px';
  304.  
  305. aniCalendarLink.prepend(calendarIcon);
  306.  
  307. aniCalendarContainer.appendChild(aniCalendarLink);
  308. activityHistoryHeader.appendChild(aniCalendarContainer);
  309.  
  310. clearInterval(interval);
  311. console.log('AniCalendar link added to Activity History.');
  312. } else {
  313. attempts++;
  314. console.log(`Activity History section header not found. Attempt ${attempts}/${maxAttempts}. Retrying in 2 seconds...`);
  315. if (attempts >= maxAttempts) {
  316. clearInterval(interval);
  317. console.warn('Failed to find Activity History section header after multiple attempts.');
  318. }
  319. }
  320. }, 2000);
  321. }
  322.  
  323. // Function to add AniTools link in Social tab
  324. function addAniToolsLink() {
  325. const socialFilterGroup = document.querySelector('div.filter-group');
  326. if (socialFilterGroup) {
  327. // Prevent adding multiple links
  328. if (socialFilterGroup.querySelector('.ani-tools-link')) return;
  329.  
  330. const aniToolsLink = document.createElement('a');
  331. aniToolsLink.href = 'https://anitools.koopz.rocks/';
  332. aniToolsLink.target = '_blank';
  333. aniToolsLink.rel = 'noopener noreferrer';
  334. aniToolsLink.textContent = 'AniTools';
  335. aniToolsLink.style.color = 'var(--color-blue)';
  336. aniToolsLink.style.display = 'flex';
  337. aniToolsLink.style.alignItems = 'center';
  338. aniToolsLink.style.textDecoration = 'none';
  339.  
  340. const wrenchIcon = document.createElement('i');
  341. wrenchIcon.className = 'fa fa-tools';
  342. wrenchIcon.style.marginRight = '5px';
  343.  
  344. aniToolsLink.prepend(wrenchIcon);
  345.  
  346. const aniToolsContainer = document.createElement('span');
  347. aniToolsContainer.classList.add('ani-tools-link');
  348. aniToolsContainer.appendChild(aniToolsLink);
  349. socialFilterGroup.appendChild(aniToolsContainer);
  350. } else {
  351. console.log('Social filter group not found.');
  352. }
  353. }
  354.  
  355. // Function to initialize features based on current URL
  356. function initializeFeatures() {
  357. const url = window.location.href;
  358.  
  359. // Check if the page is an AniList user profile
  360. if (url.includes('/user/') && !url.includes('/social')) {
  361. addAniCalendarLink();
  362. }
  363.  
  364. // Check if the page is the social tab of an AniList user profile
  365. if (url.includes('/user/') && url.includes('/social')) {
  366. addAniToolsLink();
  367. }
  368.  
  369. // Check if the page is an AniList forum thread comment
  370. if (url.includes('/forum/thread/') && url.includes('/comment/')) {
  371. createStickyBoxLink();
  372. } else {
  373. const existingStickyBox = document.querySelector('.sticky-box');
  374. if (existingStickyBox) {
  375. existingStickyBox.remove();
  376. }
  377. }
  378. }
  379.  
  380. // Function to handle URL changes
  381. function onUrlChange(callback) {
  382. let lastUrl = location.href;
  383. const observer = new MutationObserver(() => {
  384. const currentUrl = location.href;
  385. if (currentUrl !== lastUrl) {
  386. lastUrl = currentUrl;
  387. callback();
  388. }
  389. });
  390.  
  391. observer.observe(document, { subtree: true, childList: true });
  392.  
  393. window.addEventListener('popstate', () => {
  394. callback();
  395. });
  396.  
  397. const pushState = history.pushState;
  398. const replaceState = history.replaceState;
  399.  
  400. history.pushState = function () {
  401. pushState.apply(history, arguments);
  402. callback();
  403. };
  404.  
  405. history.replaceState = function () {
  406. replaceState.apply(history, arguments);
  407. callback();
  408. };
  409. }
  410.  
  411. // Initialize features on initial load
  412. window.addEventListener('load', () => {
  413. setTimeout(() => {
  414. initializeFeatures();
  415. }, 1000);
  416. });
  417.  
  418. // Initialize features on URL changes
  419. onUrlChange(() => {
  420. setTimeout(() => {
  421. initializeFeatures();
  422. }, 1000);
  423. });
  424.  
  425. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址