ShikiSearch+

Добавляет больше ссылок в раздел "На других сайтах"

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         ShikiSearch+
// @icon         https://www.google.com/s2/favicons?domain=shikimori.me
// @namespace    https://shikimori.one
// @version      2.2
// @description  Добавляет больше ссылок в раздел "На других сайтах"
// @author       LifeH
// @match        *://shikimori.org/*
// @match        *://shikimori.one/*
// @match        *://shikimori.me/*
// @grant        none
// @license      MIT
// @require      https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.6/Sortable.min.js 
// ==/UserScript==

(function () {
    'use strict';

    const allowedPaths = ["/ranobe/", "/animes/", "/mangas/"];
    let isEditing = false;
    let editingIndex = null;

    const defaultLinks = [
        {
            title: "Hentailib",
            icon: "hentailib.me",
            link: "https://hentailib.me/catalog?q={search}",
            searchMethod: "title",
            group: "mangas",
            enabled: true,
        },
        {
            title: "Anime-joy",
            icon: "anime-joy.ru",
            link: "https://anime-joy.ru/index.php?story={search}&do=search&subaction=search",
            searchMethod: "title",
            group: "animes",
            enabled: true,
        },
        {
            title: "Smotret-anime",
            icon: "smotret-anime.org",
            link: "https://smotret-anime.org/catalog/search?q={search}",
            searchMethod: "title",
            group: "animes",
            enabled: false,
        },
        {
            title: "Anilibria",
            icon: "anilibria.tv",
            link: "https://www.anilibria.tv/release/{search}.html",
            searchMethod: "slug",
            group: "animes",
            enabled: true,
        },
        {
            title: "Anilibria Top",
            icon: "anilibria.top",
            link: "https://anilibria.top/anime/releases/release/{id}",
            searchMethod: "anilibriaApi",
            group: "animes",
            enabled: true,
        },
        {
            title: "Animego",
            icon: "animego.me",
            link: "https://animego.me/search/all?q={search}",
            searchMethod: "title",
            group: "animes",
            enabled: true,
        },
        {
            title: "Anilib",
            icon: "anilib.me",
            link: "https://anilib.me/ru/catalog?q={search}",
            searchMethod: "title",
            group: "animes",
            enabled: true,
        },
        {
            title: "Jut.su",
            icon: "jut.su",
            link: "https://jut.su/search/?searchid=1893616&text={search}&web=0#",
            searchMethod: "title",
            group: "animes",
            enabled: true,
        },

        {
            title: "rutracker",
            icon: "rutracker.org",
            link: "https://rutracker.org/forum/tracker.php?nm={search}",
            searchMethod: "title",
            group: "animes",
            enabled: true,
        },
        {
            title: "Yummy-anime",
            icon: "yummy-anime.ru",
            link: "https://yummy-anime.ru/search?word={search}",
            searchMethod: "title",
            group: "animes",
            enabled: false,
        },
        {
            title: "Animevost",
            icon: "animevost.org",
            link: "https://animevost.org/index.php?do=search&subaction=search&search_start=0&full_search=0&result_from=1&story={search}",
            searchMethod: "title",
            group: "animes",
            enabled: false,
        },
        {
            title: "Crunchyroll",
            icon: "crunchyroll.com",
            link: "https://www.crunchyroll.com/search?q={search}",
            searchMethod: "originalTitle",
            group: "animes",
            enabled: false,
        },
        {
            title: "Amedia",
            icon: "amedia.lol",
            link: "https://amedia.lol/index.php?do=search&subaction=search&from_page=0&story={search}",
            searchMethod: "title",
            group: "animes",
            enabled: false,
        },
        {
            title: "Rezka",
            icon: "rezka.ag",
            link: "https://rezka.ag/search/?do=search&subaction=search&q={search}",
            searchMethod: "title",
            group: "animes",
            enabled: false,
        },
        {
            title: "Anime1",
            icon: "anime1.best",
            link: "https://anime1.best/index.php?do=search&subaction=search&search_start=0&full_search=0&result_from=1&story={search}",
            searchMethod: "title",
            group: "animes",
            enabled: false,
        }
    ];

    function getGroup() {
        const path = window.location.pathname;
        return allowedPaths.find((p) => path.startsWith(p))?.replace(/\//g, "");
    }
    function getTitle() {
        let titleElement = document.querySelector(".head h1");
        return titleElement
            ? titleElement.childNodes[0].textContent.trim()
            : null;
    }
    function getId() {
        const pathParts = window.location.pathname.split("/");
        const idPart = pathParts[2] || "";
        const match = idPart.match(/^[a-z]*(\d+)/);
        return match ? match[1] : null;
    }
    function getOriginalTitle() {
        const titleElement = document.querySelector(".head h1");
        if (!titleElement) return null;
        const separator = titleElement.querySelector(".b-separator.inline");
        if (!separator) return null;
        const originalTitle = separator.nextSibling?.textContent?.trim();
        return originalTitle || null;
    }
    function getSlug() {
        let path = window.location.pathname;
        let match = path.match(/\/(animes|mangas|ranobe)\/z?(\d+)-(.+)/);
        return match ? match[3] : null;
    }
    function getLinks() {
        let storedLinks = JSON.parse(localStorage.getItem("userLinks"));
        if (!storedLinks) {
            storedLinks = defaultLinks;
            saveLinks(storedLinks);
            return storedLinks;
        }
        let deletedLinks = JSON.parse(localStorage.getItem("deletedLinks")) || [];
        let updated = false;
        defaultLinks.forEach((defaultLink) => {
            if (deletedLinks.includes(defaultLink.title)) return;
            if (!storedLinks.some(link => link.title === defaultLink.title)) {
                storedLinks.push(defaultLink);
                updated = true;
            }
        });
        if (updated) {
            saveLinks(storedLinks);
        }
        return storedLinks;
    }
    function saveLinks(links) {
        localStorage.setItem("userLinks", JSON.stringify(links));
    }

    function linkBuilder({ icon, link, searchMethod, group: linkGroup, title, enabled }) {
        if (!enabled) return;

        const group = getGroup();
        if (linkGroup !== group) return;

        let parentBlock = document.querySelector(".subheadline.m8")?.parentElement;
        if (!parentBlock) return;
        if (parentBlock.querySelector(`[data-shiki-search="${title}"]`)) {
            return;
        }
        let searchParam;
        if (searchMethod === "slug") {
            searchParam = getSlug();
        } else if (searchMethod === "id") {
            searchParam = getId();
        } else if (searchMethod === "originalTitle") {
            searchParam = getOriginalTitle();
        } else {
            searchParam = getTitle();
        }
        if (!searchParam) return;

        let url = link.replace("{search}", encodeURIComponent(searchParam));

        let linkContainer = document.createElement('div');
        linkContainer.className = 'b-external_link b-menu-line';

        let anchor = document.createElement('a');
        anchor.className = 'b-link';
        anchor.href = url;
        anchor.textContent = title;
        anchor.target = '_blank';

        if (icon) {
            let img = document.createElement('img');
            img.src = `https://www.google.com/s2/favicons?domain=${icon}`;
            img.style.width = '19px';
            img.style.height = '19px';
            img.style.marginRight = '5px';
            anchor.prepend(img);
        }

        linkContainer.appendChild(anchor);
        const span = document.createElement('span');
        span.dataset.shikiSearch = title;
        linkContainer.appendChild(span);
        parentBlock.appendChild(linkContainer);

        if (searchMethod === "anilibriaApi") {
            const title = getTitle();
            const apiUrl = `https://anilibria.top/api/v1/app/search/releases?query=${encodeURIComponent(title)}`;
            fetch(apiUrl)
                .then(response => response.json())
                .then(data => {
                    if (data && data.length > 0) {
                        const id = data[0].id;
                        const finalUrl = `https://anilibria.top/anime/releases/release/${id}`;
                        anchor.href = finalUrl;
                    }
                })
                .catch(error => {
                    console.error(error);
                });
        }
    }

    function init() {
        let links = getLinks();
        links.forEach(linkBuilder);
    }
    function GUI() {
        const settingsBlock = document.querySelector('.block.edit-page.misc');
        if (!settingsBlock) return;
        if (document.querySelector('.shikisearch-config')) return;

        let container = document.createElement('div');
        container.className = 'shikisearch-config';
        container.style.padding = '20px';
        container.style.border = '1px solid #ccc';
        container.style.marginTop = '20px';
        container.style.background = '#f9f9f9';
        container.style.borderRadius = '8px';
        container.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
        container.style.position = 'relative';

        container.innerHTML = `
            <h3 style="margin-bottom: 20px; text-align: center;">[ShikiSearch+] Config</h3>
            <div style="display: flex; flex-direction: column; gap: 10px;">
                <input type="text" id="title" placeholder="Название" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
                <input type="text" id="icon" placeholder="Домен (например: shikimori.one)" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
                <input type="text" id="link" placeholder="Шаблон для ссылки поиска (используйте {search})" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
                <select id="searchMethod" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
                    <option value="title">По названию</option>
                    <option value="originalTitle">По названию (оригинал)</option>
                    <option value="slug">По Slug</option>
                    <option value="id">По ID</option>
                    <option value="anilibriaApi">anilibriaApi</option>
                </select>
                <select id="group" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
                    <option value="animes">Аниме</option>
                    <option value="mangas">Манга</option>
                    <option value="ranobe">Ранобэ</option>
                </select>
                <button id="addLink" style="padding: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Добавить</button>
            </div>
            <div id="linksList" style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;"></div>
            <span class="tooltip" style="position: absolute; top: 10px; right: 10px; cursor: help;">?
                <span class="tooltiptext">
                    <strong>Информация:</strong><br>
                    "По названию": Русское названия тайтла.<br>
                    "По Slug": Использует часть ссылки.<br>
                </span>
            </span>
        `;

        settingsBlock.appendChild(container);

        let style = document.createElement('style');
        style.textContent = `
            .tooltip {
                position: relative;
                display: inline-block;
                cursor: help;
                font-size: 14px;
                color: #555;
            }
            .tooltip .tooltiptext {
                visibility: hidden;
                width: 250px;
                background-color: #555;
                color: #fff;
                text-align: left;
                border-radius: 6px;
                padding: 10px;
                position: absolute;
                z-index: 1000;
                top: 100%;
                right: 0;
                opacity: 0;
                transition: opacity 0.3s;
                font-size: 12px;
                white-space: normal;
            }
            .tooltip:hover .tooltiptext {
                visibility: visible;
                opacity: 1;
            }
        `;
        document.head.appendChild(style);

        document.getElementById('addLink').addEventListener('click', () => {
            let title = document.getElementById('title').value.trim();
            let icon = document.getElementById('icon').value.trim();
            let link = document.getElementById('link').value.trim();
            let searchMethod = document.getElementById('searchMethod').value;
            let group = document.getElementById('group').value;

            if (!title || !link) {
                alert('Заполните название и ссылку!');
                return;
            }

            let links = getLinks();

            if (isEditing) {
                links[editingIndex] = { title, icon, link, searchMethod, group, enabled: true };
                isEditing = false;
                editingIndex = null;
                document.getElementById('addLink').textContent = 'Добавить';
            } else {
                links.push({ title, icon, link, searchMethod, group, enabled: true });
            }

            saveLinks(links);
            updateLinksList();
            clearForm();
        });

        updateLinksList();
    }
    function updateLinksList() {
        let linksList = document.getElementById('linksList');
        linksList.innerHTML = '';

        let links = getLinks();
        links.forEach((link, index) => {
            let card = document.createElement('div');
            card.style.display = 'flex';
            card.style.alignItems = 'center';
            card.style.justifyContent = 'space-between';
            card.style.padding = '10px';
            card.style.border = '1px solid #ccc';
            card.style.borderRadius = '8px';
            card.style.background = '#fff';
            card.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
            card.style.cursor = 'grab';
            card.setAttribute('data-index', index);
            card.setAttribute('draggable', 'true');

            card.addEventListener('dragstart', (e) => {
                e.dataTransfer.setData('text/plain', '');
                e.target.style.opacity = '0.4';
            });

            card.addEventListener('dragend', (e) => {
                e.target.style.opacity = '1';
            });

            card.innerHTML = `
                <div style="display: flex; align-items: center; gap: 10px;">
                    <img src="https://www.google.com/s2/favicons?domain=${link.icon}" style="width: 16px; height: 16px;">
                    <span>${link.title} (${link.group})</span>
                </div>
                <div style="display: flex; align-items: center; gap: 10px;">
                    <label class="toggle-switch">
                        <input type="checkbox" ${link.enabled ? 'checked' : ''} onchange="toggleLink(${index}, this.checked)">
                        <span class="slider"></span>
                    </label>
                    <button onclick="editLink(${index})" style="padding: 5px 10px; background-color: #FFC107; color: white; border: none; border-radius: 4px; cursor: pointer;">Редактировать</button>
                    <button onclick="deleteLink(${index})" style="padding: 5px 10px; background-color: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Удалить</button>
                </div>
            `;

            linksList.appendChild(card);
        });

        new Sortable(linksList, {
            animation: 150,
            onEnd: function (evt) {
                let links = getLinks();
                let movedItem = links.splice(evt.oldIndex, 1)[0];
                links.splice(evt.newIndex, 0, movedItem);
                saveLinks(links);
                updateLinksList();
            }
        });

        let style = document.createElement('style');
        style.textContent = `
            .toggle-switch {
                position: relative;
                display: inline-block;
                width: 40px;
                height: 20px;
            }
            .toggle-switch input {
                opacity: 0;
                width: 0;
                height: 0;
            }
            .slider {
                position: absolute;
                cursor: pointer;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background-color: #ccc;
                transition: 0.4s;
                border-radius: 20px;
            }
            .slider:before {
                position: absolute;
                content: "";
                height: 16px;
                width: 16px;
                left: 2px;
                bottom: 2px;
                background-color: white;
                transition: 0.4s;
                border-radius: 50%;
            }
            input:checked + .slider {
                background-color: #4CAF50;
            }
            input:checked + .slider:before {
                transform: translateX(20px);
            }
 
            .sortable-chosen {
                background-color: #f0f0f0;
            }
            .sortable-ghost {
                opacity: 0.5;
            }
        `;
        document.head.appendChild(style);
    }
    function clearForm() {
        document.getElementById('title').value = '';
        document.getElementById('icon').value = '';
        document.getElementById('link').value = '';
        document.getElementById('searchMethod').value = 'title';
        document.getElementById('group').value = 'animes';
    }
    window.deleteLink = function (index) {
        if (confirm('Вы уверены, что хотите удалить эту ссылку?')) {
            let links = getLinks();
            let deletedLink = links[index];
            if (defaultLinks.some(link => link.title === deletedLink.title)) {
                let deletedLinks = JSON.parse(localStorage.getItem("deletedLinks")) || [];
                if (!deletedLinks.includes(deletedLink.title)) {
                    deletedLinks.push(deletedLink.title);
                    localStorage.setItem("deletedLinks", JSON.stringify(deletedLinks));
                }
            }
            links.splice(index, 1);
            saveLinks(links);
            updateLinksList();
        }
    };
    window.editLink = function (index) {
        let links = getLinks();
        let link = links[index];

        document.getElementById('title').value = link.title;
        document.getElementById('icon').value = link.icon;
        document.getElementById('link').value = link.link;
        document.getElementById('searchMethod').value = link.searchMethod;
        document.getElementById('group').value = link.group;

        isEditing = true;
        editingIndex = index;
        document.getElementById('addLink').textContent = 'Сохранить изменения';
    };
    window.toggleLink = function (index, enabled) {
        let links = getLinks();
        links[index].enabled = enabled;
        saveLinks(links);
    };
    function ready(fn) {
        document.addEventListener('page:load', fn);
        document.addEventListener('turbolinks:load', fn);
        if (document.readyState !== "loading") {
            fn();
        } else {
            document.addEventListener('DOMContentLoaded', fn);
        }
    }

    ready(init);
    ready(GUI);
})();