// ==UserScript==
// @name ShikiSearch+
// @icon https://www.google.com/s2/favicons?domain=shikimori.me
// @namespace https://shikimori.one
// @version 1.0
// @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: '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: '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: 'title',
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 getSlug() {
let path = window.location.pathname;
let match = path.match(/\/(animes|mangas|ranobe)\/z?(\d+)-(.+)/);
return match ? match[3] : null;
}
function getLinks() {
let links = JSON.parse(localStorage.getItem('userLinks'));
if (!links) {
links = defaultLinks;
saveLinks(links);
}
return links;
}
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 {
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);
}
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="slug">По Slug</option>
<option value="id">По ID</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();
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);
})();