AniList Shortcuts

Add multiples shortcuts + custom ones

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AniList Shortcuts
// @version      1.0
// @description  Add multiples shortcuts + custom ones
// @author       Mio.
// @namespace    https://github.com/dear-clouds/mio-userscripts
// @supportURL   https://github.com/dear-clouds/mio-userscripts/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=anilist.co
// @license      GPL-3.0
// @match        *://*.anilist.co/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // Function to inject Font Awesome CSS
    function injectFontAwesome() {
        const faLink = document.createElement('link');
        faLink.rel = 'stylesheet';
        faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
        faLink.integrity = 'sha512-pap5K1fL5c4sLcXmpopbPWha8z36H1EJGgUK6YyE1Wfo2jydN12wPuABanVbBv8d5kZdO8+8PpJ1f8kz0gJ0Mg==';
        faLink.crossOrigin = 'anonymous';
        faLink.referrerPolicy = 'no-referrer';
        document.head.appendChild(faLink);
    }

    injectFontAwesome();

    // Utility function to wait for an element to appear in the DOM
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            let timeElapsed = 0;

            const interval = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(interval);
                    resolve(element);
                }
                timeElapsed += intervalTime;
                if (timeElapsed >= timeout) {
                    clearInterval(interval);
                    reject(`Element ${selector} not found within ${timeout}ms`);
                }
            }, intervalTime);
        });
    }

    // Function to add a link with Font Awesome icon
    function addLinkWithIcon(element, url, linkText, iconName) {
        const link = document.createElement('a');
        link.href = url;
        link.target = '_blank';
        link.rel = 'noopener noreferrer';
        link.style.textDecoration = 'none';
        link.style.color = 'inherit';
        link.style.display = 'flex';
        link.style.alignItems = 'center';
        link.style.marginLeft = '10px';

        const icon = document.createElement('i');
        icon.className = `fa fa-${iconName}`;
        icon.style.marginRight = '5px';

        link.appendChild(icon);
        link.appendChild(document.createTextNode(linkText));
        element.appendChild(link);
    }

    // Function to create a sticky box on forum comments
    function createStickyBoxLink() {
        // Prevent multiple sticky boxes
        if (document.querySelector('.sticky-box')) return;

        const stickyBox = document.createElement('div');
        stickyBox.classList.add('sticky-box');
        stickyBox.style.position = 'fixed';
        stickyBox.style.right = '10px';
        stickyBox.style.top = '200px';
        stickyBox.style.width = '200px';
        stickyBox.style.padding = '10px';
        stickyBox.style.backgroundColor = 'rgb(var(--color-foreground))';
        stickyBox.style.borderRadius = '4px';
        stickyBox.style.transition = 'height 0.5s, opacity 0.5s';
        stickyBox.style.overflow = 'hidden';
        stickyBox.style.zIndex = '1000';

        const header = document.createElement('h3');
        header.innerText = 'Shortcuts';
        header.style.fontSize = 'medium';
        header.style.marginTop = '5';
        stickyBox.appendChild(header);

        const linksContainer = document.createElement('div');
        stickyBox.appendChild(linksContainer);

        const addIcon = document.createElement('span');
        addIcon.innerText = '+';
        addIcon.style.position = 'absolute';
        addIcon.style.top = '5px';
        addIcon.style.right = '5px';
        addIcon.style.cursor = 'pointer';
        addIcon.style.fontWeight = 'bold';
        addIcon.onclick = function () {
            const isHidden = userInput.style.display === 'none';
            userInput.style.display = isHidden ? 'block' : 'none';
            shortcutNameInput.style.display = isHidden ? 'block' : 'none';
            validateButton.style.display = isHidden ? 'block' : 'none';
        };
        stickyBox.appendChild(addIcon);

        const toggleVisibilityIcon = document.createElement('i');
        toggleVisibilityIcon.className = 'fa fa-eye';
        toggleVisibilityIcon.style.cursor = 'pointer';
        toggleVisibilityIcon.style.position = 'absolute';
        toggleVisibilityIcon.style.top = '5px';
        toggleVisibilityIcon.style.left = '5px';
        toggleVisibilityIcon.style.fontSize = '14px';
        toggleVisibilityIcon.onclick = function () {
            if (stickyBox.style.height !== '25px') {
                stickyBox.style.width = '25px';
                stickyBox.style.height = '25px';
                linksContainer.style.display = 'none';
                header.style.display = 'none';
                addIcon.style.display = 'none';
                toggleVisibilityIcon.className = 'fa fa-eye-slash';
            } else {
                stickyBox.style.width = '200px';
                stickyBox.style.height = 'auto';
                linksContainer.style.display = 'block';
                header.style.display = 'block';
                addIcon.style.display = 'block';
                toggleVisibilityIcon.className = 'fa fa-eye';
            }
        };
        stickyBox.appendChild(toggleVisibilityIcon);

        function appendLinkToContainer(linkName, linkURL) {
            const linkElement = document.createElement('a');
            linkElement.href = linkURL;
            linkElement.innerText = linkName;
            linkElement.style.fontSize = 'smaller';
            linkElement.target = '_blank';
            linkElement.style.display = 'flex';
            linkElement.style.alignItems = 'center';
            linkElement.style.marginBottom = '5px';
            linkElement.style.color = 'var(--color-blue)';
            linkElement.style.textDecoration = 'none';

            linkElement.addEventListener('click', (e) => {
                e.preventDefault();
                window.open(linkURL, '_blank');
            });

            const favicon = document.createElement('img');
            try {
                const urlObj = new URL(linkURL);
                favicon.src = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}`;
            } catch {
                favicon.src = '';
            }
            favicon.style.marginRight = '5px';
            favicon.style.width = '16px';
            favicon.style.height = '16px';
            linkElement.prepend(favicon);

            const deleteIcon = document.createElement('span');
            deleteIcon.innerText = ' ×';
            deleteIcon.style.color = 'rgb(var(--color-blue))';
            deleteIcon.style.cursor = 'pointer';
            deleteIcon.style.marginLeft = 'auto';
            deleteIcon.onclick = function (event) {
                event.stopPropagation();
                linksContainer.removeChild(linkElement);
                const savedLinks = JSON.parse(localStorage.getItem('MioAniListShortcuts') || '[]');
                const updatedLinks = savedLinks.filter(l => l.url !== linkURL);
                localStorage.setItem('MioAniListShortcuts', JSON.stringify(updatedLinks));
            };
            linkElement.appendChild(deleteIcon);

            linksContainer.appendChild(linkElement);
        }

        const userInput = document.createElement('input');
        userInput.type = 'text';
        userInput.placeholder = 'Enter your link';
        userInput.style.display = 'none';
        userInput.style.backgroundColor = 'rgb(var(--color-background))';
        userInput.style.color = 'rgb(var(--color-blue))';
        userInput.style.border = '1px solid var(--color-border)';
        userInput.style.borderRadius = '3px';
        userInput.style.padding = '5px';
        userInput.style.fontSize = 'smaller';
        userInput.style.marginTop = '5px';
        stickyBox.appendChild(userInput);

        const shortcutNameInput = document.createElement('input');
        shortcutNameInput.type = 'text';
        shortcutNameInput.placeholder = 'Name of the shortcut';
        shortcutNameInput.style.display = 'none';
        shortcutNameInput.style.backgroundColor = 'rgb(var(--color-background))';
        shortcutNameInput.style.color = 'rgb(var(--color-blue))';
        shortcutNameInput.style.border = '1px solid var(--color-border)';
        shortcutNameInput.style.borderRadius = '3px';
        shortcutNameInput.style.padding = '5px';
        shortcutNameInput.style.fontSize = 'smaller';
        shortcutNameInput.style.marginTop = '5px';
        stickyBox.appendChild(shortcutNameInput);

        const validateButton = document.createElement('button');
        validateButton.innerText = 'Add';
        validateButton.style.display = 'none';
        validateButton.style.backgroundColor = 'var(--color-button)';
        validateButton.style.color = 'var(--color-button-text)';
        validateButton.style.border = 'none';
        validateButton.style.borderRadius = '3px';
        validateButton.style.padding = '5px 10px';
        validateButton.style.fontSize = 'smaller';
        validateButton.style.marginTop = '5px';
        validateButton.style.cursor = 'pointer';
        validateButton.onclick = function () {
            const link = userInput.value.trim();
            const name = shortcutNameInput.value.trim();
            if (link && name) {
                const savedLinks = JSON.parse(localStorage.getItem('MioAniListShortcuts') || '[]');
                // Avoid duplicates
                if (!savedLinks.some(l => l.url === link)) {
                    savedLinks.push({ name, url: link });
                    localStorage.setItem('MioAniListShortcuts', JSON.stringify(savedLinks));

                    appendLinkToContainer(name, link);

                    userInput.value = '';
                    shortcutNameInput.value = '';
                    userInput.style.display = 'none';
                    shortcutNameInput.style.display = 'none';
                    validateButton.style.display = 'none';
                } else {
                    alert('This link already exists in your shortcuts.');
                }
            } else {
                alert('Please enter both name and URL.');
            }
        };
        stickyBox.appendChild(validateButton);

        // Load saved links
        const savedLinks = JSON.parse(localStorage.getItem('MioAniListShortcuts') || '[]');
        for (const linkObj of savedLinks) {
            appendLinkToContainer(linkObj.name, linkObj.url);
        }

        document.body.appendChild(stickyBox);
    }

    // Function to add AniCalendar by KangieDanie link in Activity History 
    // https://anilist.co/forum/thread/63096
    function addAniCalendarLink() {
        // Prevent adding multiple links
        if (document.querySelector('.ani-calendar-link')) return;

        let attempts = 0;
        const maxAttempts = 10; // 20 seconds max
        const interval = setInterval(() => {
            const headers = document.querySelectorAll('h2.section-header');
            let activityHistoryHeader = null;

            headers.forEach(header => {
                if (header.textContent.trim() === 'Activity History') {
                    activityHistoryHeader = header;
                }
            });

            if (activityHistoryHeader) {
                // Prevent adding multiple links
                if (activityHistoryHeader.querySelector('.ani-calendar-link')) {
                    clearInterval(interval);
                    return;
                }

                const aniCalendarContainer = document.createElement('span');
                aniCalendarContainer.classList.add('ani-calendar-link');
                aniCalendarContainer.style.float = 'right';
                aniCalendarContainer.style.display = 'flex';
                aniCalendarContainer.style.alignItems = 'center';

                const aniCalendarLink = document.createElement('a');
                aniCalendarLink.href = 'https://ani-calendar.vercel.app/';
                aniCalendarLink.target = '_blank';
                aniCalendarLink.rel = 'noopener noreferrer';
                aniCalendarLink.textContent = 'AniCalendar';
                aniCalendarLink.style.fontSize = 'smaller';
                aniCalendarLink.style.marginLeft = '10px';
                aniCalendarLink.style.color = 'var(--color-blue)';
                aniCalendarLink.style.display = 'flex';
                aniCalendarLink.style.alignItems = 'center';
                aniCalendarLink.style.textDecoration = 'none';

                const calendarIcon = document.createElement('i');
                calendarIcon.className = 'fa fa-calendar';
                calendarIcon.style.marginRight = '5px';

                aniCalendarLink.prepend(calendarIcon);

                aniCalendarContainer.appendChild(aniCalendarLink);
                activityHistoryHeader.appendChild(aniCalendarContainer);

                clearInterval(interval);
                console.log('AniCalendar link added to Activity History.');
            } else {
                attempts++;
                console.log(`Activity History section header not found. Attempt ${attempts}/${maxAttempts}. Retrying in 2 seconds...`);
                if (attempts >= maxAttempts) {
                    clearInterval(interval);
                    console.warn('Failed to find Activity History section header after multiple attempts.');
                }
            }
        }, 2000);
    }

    // Function to add AniTools link in Social tab
    function addAniToolsLink() {
        const socialFilterGroup = document.querySelector('div.filter-group');
        if (socialFilterGroup) {
            // Prevent adding multiple links
            if (socialFilterGroup.querySelector('.ani-tools-link')) return;

            const aniToolsLink = document.createElement('a');
            aniToolsLink.href = 'https://anitools.koopz.rocks/';
            aniToolsLink.target = '_blank';
            aniToolsLink.rel = 'noopener noreferrer';
            aniToolsLink.textContent = 'AniTools';
            aniToolsLink.style.color = 'var(--color-blue)';
            aniToolsLink.style.display = 'flex';
            aniToolsLink.style.alignItems = 'center';
            aniToolsLink.style.textDecoration = 'none';

            const wrenchIcon = document.createElement('i');
            wrenchIcon.className = 'fa fa-tools';
            wrenchIcon.style.marginRight = '5px';

            aniToolsLink.prepend(wrenchIcon);

            const aniToolsContainer = document.createElement('span');
            aniToolsContainer.classList.add('ani-tools-link');
            aniToolsContainer.appendChild(aniToolsLink);
            socialFilterGroup.appendChild(aniToolsContainer);
        } else {
            console.log('Social filter group not found.');
        }
    }

    // Function to initialize features based on current URL
    function initializeFeatures() {
        const url = window.location.href;

        // Check if the page is an AniList user profile
        if (url.includes('/user/') && !url.includes('/social')) {
            addAniCalendarLink();
        }

        // Check if the page is the social tab of an AniList user profile
        if (url.includes('/user/') && url.includes('/social')) {
            addAniToolsLink();
        }

        // Check if the page is an AniList forum thread comment
        if (url.includes('/forum/thread/') && url.includes('/comment/')) {
            createStickyBoxLink();
        } else {
            const existingStickyBox = document.querySelector('.sticky-box');
            if (existingStickyBox) {
                existingStickyBox.remove();
            }
        }
    }

    // Function to handle URL changes
    function onUrlChange(callback) {
        let lastUrl = location.href;
        const observer = new MutationObserver(() => {
            const currentUrl = location.href;
            if (currentUrl !== lastUrl) {
                lastUrl = currentUrl;
                callback();
            }
        });

        observer.observe(document, { subtree: true, childList: true });

        window.addEventListener('popstate', () => {
            callback();
        });

        const pushState = history.pushState;
        const replaceState = history.replaceState;

        history.pushState = function () {
            pushState.apply(history, arguments);
            callback();
        };

        history.replaceState = function () {
            replaceState.apply(history, arguments);
            callback();
        };
    }

    // Initialize features on initial load
    window.addEventListener('load', () => {
        setTimeout(() => {
            initializeFeatures();
        }, 1000);
    });

    // Initialize features on URL changes
    onUrlChange(() => {
        setTimeout(() => {
            initializeFeatures();
        }, 1000);
    });

})();