ShikiSearch+

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

// ==UserScript==
// @name         ShikiSearch+
// @icon         https://www.google.com/s2/favicons?domain=shikimori.me
// @namespace    https://shikimori.one
// @version      1.7
// @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: "Anime-joy",
        icon: "anime-joy.online",
        link: "https://anime-joy.online/index.php?story={search}&do=search&subaction=search",
        searchMethod: "title",
        group: "animes",
        enabled: true,
      },
      {
        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;

        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);
        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);
})();