ShikiSearch+

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

  1. // ==UserScript==
  2. // @name ShikiSearch+
  3. // @icon https://www.google.com/s2/favicons?domain=shikimori.me
  4. // @namespace https://shikimori.one
  5. // @version 1.8
  6. // @description Добавляет больше ссылок в раздел "На других сайтах"
  7. // @author LifeH
  8. // @match *://shikimori.org/*
  9. // @match *://shikimori.one/*
  10. // @match *://shikimori.me/*
  11. // @grant none
  12. // @license MIT
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.6/Sortable.min.js
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. const allowedPaths = ["/ranobe/", "/animes/", "/mangas/"];
  20. let isEditing = false;
  21. let editingIndex = null;
  22.  
  23. const defaultLinks = [
  24. {
  25. title: "Anime-joy",
  26. icon: "anime-joy.ru",
  27. link: "https://anime-joy.ru/index.php?story={search}&do=search&subaction=search",
  28. searchMethod: "title",
  29. group: "animes",
  30. enabled: true,
  31. },
  32. {
  33. title: "Anilibria",
  34. icon: "anilibria.tv",
  35. link: "https://www.anilibria.tv/release/{search}.html",
  36. searchMethod: "slug",
  37. group: "animes",
  38. enabled: true,
  39. },
  40. {
  41. title: "Anilibria Top",
  42. icon: "anilibria.top",
  43. link: "https://anilibria.top/anime/releases/release/{id}",
  44. searchMethod: "anilibriaApi",
  45. group: "animes",
  46. enabled: true,
  47. },
  48. {
  49. title: "Animego",
  50. icon: "animego.me",
  51. link: "https://animego.me/search/all?q={search}",
  52. searchMethod: "title",
  53. group: "animes",
  54. enabled: true,
  55. },
  56. {
  57. title: "Anilib",
  58. icon: "anilib.me",
  59. link: "https://anilib.me/ru/catalog?q={search}",
  60. searchMethod: "title",
  61. group: "animes",
  62. enabled: true,
  63. },
  64. {
  65. title: "Jut.su",
  66. icon: "jut.su",
  67. link: "https://jut.su/search/?searchid=1893616&text={search}&web=0#",
  68. searchMethod: "title",
  69. group: "animes",
  70. enabled: true,
  71. },
  72.  
  73. {
  74. title: "rutracker",
  75. icon: "rutracker.org",
  76. link: "https://rutracker.org/forum/tracker.php?nm={search}",
  77. searchMethod: "title",
  78. group: "animes",
  79. enabled: true,
  80. },
  81. {
  82. title: "Yummy-anime",
  83. icon: "yummy-anime.ru",
  84. link: "https://yummy-anime.ru/search?word={search}",
  85. searchMethod: "title",
  86. group: "animes",
  87. enabled: false,
  88. },
  89. {
  90. title: "Animevost",
  91. icon: "animevost.org",
  92. link: "https://animevost.org/index.php?do=search&subaction=search&search_start=0&full_search=0&result_from=1&story={search}",
  93. searchMethod: "title",
  94. group: "animes",
  95. enabled: false,
  96. },
  97. {
  98. title: "Crunchyroll",
  99. icon: "crunchyroll.com",
  100. link: "https://www.crunchyroll.com/search?q={search}",
  101. searchMethod: "originalTitle",
  102. group: "animes",
  103. enabled: false,
  104. },
  105. {
  106. title: "Amedia",
  107. icon: "amedia.lol",
  108. link: "https://amedia.lol/index.php?do=search&subaction=search&from_page=0&story={search}",
  109. searchMethod: "title",
  110. group: "animes",
  111. enabled: false,
  112. },
  113. {
  114. title: "Rezka",
  115. icon: "rezka.ag",
  116. link: "https://rezka.ag/search/?do=search&subaction=search&q={search}",
  117. searchMethod: "title",
  118. group: "animes",
  119. enabled: false,
  120. },
  121. {
  122. title: "Anime1",
  123. icon: "anime1.best",
  124. link: "https://anime1.best/index.php?do=search&subaction=search&search_start=0&full_search=0&result_from=1&story={search}",
  125. searchMethod: "title",
  126. group: "animes",
  127. enabled: false,
  128. }
  129. ];
  130.  
  131. function getGroup() {
  132. const path = window.location.pathname;
  133. return allowedPaths.find((p) => path.startsWith(p))?.replace(/\//g, "");
  134. }
  135. function getTitle() {
  136. let titleElement = document.querySelector(".head h1");
  137. return titleElement
  138. ? titleElement.childNodes[0].textContent.trim()
  139. : null;
  140. }
  141. function getId() {
  142. const pathParts = window.location.pathname.split("/");
  143. const idPart = pathParts[2] || "";
  144. const match = idPart.match(/^[a-z]*(\d+)/);
  145. return match ? match[1] : null;
  146. }
  147. function getOriginalTitle() {
  148. const titleElement = document.querySelector(".head h1");
  149. if (!titleElement) return null;
  150. const separator = titleElement.querySelector(".b-separator.inline");
  151. if (!separator) return null;
  152. const originalTitle = separator.nextSibling?.textContent?.trim();
  153. return originalTitle || null;
  154. }
  155. function getSlug() {
  156. let path = window.location.pathname;
  157. let match = path.match(/\/(animes|mangas|ranobe)\/z?(\d+)-(.+)/);
  158. return match ? match[3] : null;
  159. }
  160. function getLinks() {
  161. let storedLinks = JSON.parse(localStorage.getItem("userLinks"));
  162. if (!storedLinks) {
  163. storedLinks = defaultLinks;
  164. saveLinks(storedLinks);
  165. return storedLinks;
  166. }
  167. let deletedLinks = JSON.parse(localStorage.getItem("deletedLinks")) || [];
  168. let updated = false;
  169. defaultLinks.forEach((defaultLink) => {
  170. if (deletedLinks.includes(defaultLink.title)) return;
  171. if (!storedLinks.some(link => link.title === defaultLink.title)) {
  172. storedLinks.push(defaultLink);
  173. updated = true;
  174. }
  175. });
  176. if (updated) {
  177. saveLinks(storedLinks);
  178. }
  179. return storedLinks;
  180. }
  181. function saveLinks(links) {
  182. localStorage.setItem("userLinks", JSON.stringify(links));
  183. }
  184.  
  185. function linkBuilder({ icon, link, searchMethod, group: linkGroup, title, enabled }) {
  186. if (!enabled) return;
  187.  
  188. const group = getGroup();
  189. if (linkGroup !== group) return;
  190.  
  191. let parentBlock = document.querySelector(".subheadline.m8")?.parentElement;
  192. if (!parentBlock) return;
  193.  
  194. let searchParam;
  195. if (searchMethod === "slug") {
  196. searchParam = getSlug();
  197. } else if (searchMethod === "id") {
  198. searchParam = getId();
  199. } else if (searchMethod === "originalTitle") {
  200. searchParam = getOriginalTitle();
  201. } else {
  202. searchParam = getTitle();
  203. }
  204. if (!searchParam) return;
  205.  
  206. let url = link.replace("{search}", encodeURIComponent(searchParam));
  207.  
  208. let linkContainer = document.createElement('div');
  209. linkContainer.className = 'b-external_link b-menu-line';
  210.  
  211. let anchor = document.createElement('a');
  212. anchor.className = 'b-link';
  213. anchor.href = url;
  214. anchor.textContent = title;
  215. anchor.target = '_blank';
  216.  
  217. if (icon) {
  218. let img = document.createElement('img');
  219. img.src = `https://www.google.com/s2/favicons?domain=${icon}`;
  220. img.style.width = '19px';
  221. img.style.height = '19px';
  222. img.style.marginRight = '5px';
  223. anchor.prepend(img);
  224. }
  225.  
  226. linkContainer.appendChild(anchor);
  227. parentBlock.appendChild(linkContainer);
  228.  
  229. if (searchMethod === "anilibriaApi") {
  230. const title = getTitle();
  231. const apiUrl = `https://anilibria.top/api/v1/app/search/releases?query=${encodeURIComponent(title)}`;
  232. fetch(apiUrl)
  233. .then(response => response.json())
  234. .then(data => {
  235. if (data && data.length > 0) {
  236. const id = data[0].id;
  237. const finalUrl = `https://anilibria.top/anime/releases/release/${id}`;
  238. anchor.href = finalUrl;
  239. }
  240. })
  241. .catch(error => {
  242. console.error(error);
  243. });
  244. }
  245. }
  246.  
  247. function init() {
  248. let links = getLinks();
  249. links.forEach(linkBuilder);
  250. }
  251. function GUI() {
  252. const settingsBlock = document.querySelector('.block.edit-page.misc');
  253. if (!settingsBlock) return;
  254. if (document.querySelector('.shikisearch-config')) return;
  255.  
  256. let container = document.createElement('div');
  257. container.className = 'shikisearch-config';
  258. container.style.padding = '20px';
  259. container.style.border = '1px solid #ccc';
  260. container.style.marginTop = '20px';
  261. container.style.background = '#f9f9f9';
  262. container.style.borderRadius = '8px';
  263. container.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  264. container.style.position = 'relative';
  265.  
  266. container.innerHTML = `
  267. <h3 style="margin-bottom: 20px; text-align: center;">[ShikiSearch+] Config</h3>
  268. <div style="display: flex; flex-direction: column; gap: 10px;">
  269. <input type="text" id="title" placeholder="Название" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
  270. <input type="text" id="icon" placeholder="Домен (например: shikimori.one)" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
  271. <input type="text" id="link" placeholder="Шаблон для ссылки поиска (используйте {search})" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
  272. <select id="searchMethod" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
  273. <option value="title">По названию</option>
  274. <option value="originalTitle">По названию (оригинал)</option>
  275. <option value="slug">По Slug</option>
  276. <option value="id">По ID</option>
  277. <option value="anilibriaApi">anilibriaApi</option>
  278. </select>
  279. <select id="group" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
  280. <option value="animes">Аниме</option>
  281. <option value="mangas">Манга</option>
  282. <option value="ranobe">Ранобэ</option>
  283. </select>
  284. <button id="addLink" style="padding: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Добавить</button>
  285. </div>
  286. <div id="linksList" style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;"></div>
  287. <span class="tooltip" style="position: absolute; top: 10px; right: 10px; cursor: help;">?
  288. <span class="tooltiptext">
  289. <strong>Информация:</strong><br>
  290. "По названию": Русское названия тайтла.<br>
  291. "По Slug": Использует часть ссылки.<br>
  292. </span>
  293. </span>
  294. `;
  295.  
  296. settingsBlock.appendChild(container);
  297.  
  298. let style = document.createElement('style');
  299. style.textContent = `
  300. .tooltip {
  301. position: relative;
  302. display: inline-block;
  303. cursor: help;
  304. font-size: 14px;
  305. color: #555;
  306. }
  307. .tooltip .tooltiptext {
  308. visibility: hidden;
  309. width: 250px;
  310. background-color: #555;
  311. color: #fff;
  312. text-align: left;
  313. border-radius: 6px;
  314. padding: 10px;
  315. position: absolute;
  316. z-index: 1000;
  317. top: 100%;
  318. right: 0;
  319. opacity: 0;
  320. transition: opacity 0.3s;
  321. font-size: 12px;
  322. white-space: normal;
  323. }
  324. .tooltip:hover .tooltiptext {
  325. visibility: visible;
  326. opacity: 1;
  327. }
  328. `;
  329. document.head.appendChild(style);
  330.  
  331. document.getElementById('addLink').addEventListener('click', () => {
  332. let title = document.getElementById('title').value.trim();
  333. let icon = document.getElementById('icon').value.trim();
  334. let link = document.getElementById('link').value.trim();
  335. let searchMethod = document.getElementById('searchMethod').value;
  336. let group = document.getElementById('group').value;
  337.  
  338. if (!title || !link) {
  339. alert('Заполните название и ссылку!');
  340. return;
  341. }
  342.  
  343. let links = getLinks();
  344.  
  345. if (isEditing) {
  346. links[editingIndex] = { title, icon, link, searchMethod, group, enabled: true };
  347. isEditing = false;
  348. editingIndex = null;
  349. document.getElementById('addLink').textContent = 'Добавить';
  350. } else {
  351. links.push({ title, icon, link, searchMethod, group, enabled: true });
  352. }
  353.  
  354. saveLinks(links);
  355. updateLinksList();
  356. clearForm();
  357. });
  358.  
  359. updateLinksList();
  360. }
  361. function updateLinksList() {
  362. let linksList = document.getElementById('linksList');
  363. linksList.innerHTML = '';
  364.  
  365. let links = getLinks();
  366. links.forEach((link, index) => {
  367. let card = document.createElement('div');
  368. card.style.display = 'flex';
  369. card.style.alignItems = 'center';
  370. card.style.justifyContent = 'space-between';
  371. card.style.padding = '10px';
  372. card.style.border = '1px solid #ccc';
  373. card.style.borderRadius = '8px';
  374. card.style.background = '#fff';
  375. card.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  376. card.style.cursor = 'grab';
  377. card.setAttribute('data-index', index);
  378. card.setAttribute('draggable', 'true');
  379.  
  380. card.addEventListener('dragstart', (e) => {
  381. e.dataTransfer.setData('text/plain', '');
  382. e.target.style.opacity = '0.4';
  383. });
  384.  
  385. card.addEventListener('dragend', (e) => {
  386. e.target.style.opacity = '1';
  387. });
  388.  
  389. card.innerHTML = `
  390. <div style="display: flex; align-items: center; gap: 10px;">
  391. <img src="https://www.google.com/s2/favicons?domain=${link.icon}" style="width: 16px; height: 16px;">
  392. <span>${link.title} (${link.group})</span>
  393. </div>
  394. <div style="display: flex; align-items: center; gap: 10px;">
  395. <label class="toggle-switch">
  396. <input type="checkbox" ${link.enabled ? 'checked' : ''} onchange="toggleLink(${index}, this.checked)">
  397. <span class="slider"></span>
  398. </label>
  399. <button onclick="editLink(${index})" style="padding: 5px 10px; background-color: #FFC107; color: white; border: none; border-radius: 4px; cursor: pointer;">Редактировать</button>
  400. <button onclick="deleteLink(${index})" style="padding: 5px 10px; background-color: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Удалить</button>
  401. </div>
  402. `;
  403.  
  404. linksList.appendChild(card);
  405. });
  406.  
  407. new Sortable(linksList, {
  408. animation: 150,
  409. onEnd: function (evt) {
  410. let links = getLinks();
  411. let movedItem = links.splice(evt.oldIndex, 1)[0];
  412. links.splice(evt.newIndex, 0, movedItem);
  413. saveLinks(links);
  414. updateLinksList();
  415. }
  416. });
  417.  
  418. let style = document.createElement('style');
  419. style.textContent = `
  420. .toggle-switch {
  421. position: relative;
  422. display: inline-block;
  423. width: 40px;
  424. height: 20px;
  425. }
  426. .toggle-switch input {
  427. opacity: 0;
  428. width: 0;
  429. height: 0;
  430. }
  431. .slider {
  432. position: absolute;
  433. cursor: pointer;
  434. top: 0;
  435. left: 0;
  436. right: 0;
  437. bottom: 0;
  438. background-color: #ccc;
  439. transition: 0.4s;
  440. border-radius: 20px;
  441. }
  442. .slider:before {
  443. position: absolute;
  444. content: "";
  445. height: 16px;
  446. width: 16px;
  447. left: 2px;
  448. bottom: 2px;
  449. background-color: white;
  450. transition: 0.4s;
  451. border-radius: 50%;
  452. }
  453. input:checked + .slider {
  454. background-color: #4CAF50;
  455. }
  456. input:checked + .slider:before {
  457. transform: translateX(20px);
  458. }
  459.  
  460. .sortable-chosen {
  461. background-color: #f0f0f0;
  462. }
  463. .sortable-ghost {
  464. opacity: 0.5;
  465. }
  466. `;
  467. document.head.appendChild(style);
  468. }
  469. function clearForm() {
  470. document.getElementById('title').value = '';
  471. document.getElementById('icon').value = '';
  472. document.getElementById('link').value = '';
  473. document.getElementById('searchMethod').value = 'title';
  474. document.getElementById('group').value = 'animes';
  475. }
  476. window.deleteLink = function(index) {
  477. if (confirm('Вы уверены, что хотите удалить эту ссылку?')) {
  478. let links = getLinks();
  479. let deletedLink = links[index];
  480. if (defaultLinks.some(link => link.title === deletedLink.title)) {
  481. let deletedLinks = JSON.parse(localStorage.getItem("deletedLinks")) || [];
  482. if (!deletedLinks.includes(deletedLink.title)) {
  483. deletedLinks.push(deletedLink.title);
  484. localStorage.setItem("deletedLinks", JSON.stringify(deletedLinks));
  485. }
  486. }
  487. links.splice(index, 1);
  488. saveLinks(links);
  489. updateLinksList();
  490. }
  491. };
  492. window.editLink = function(index) {
  493. let links = getLinks();
  494. let link = links[index];
  495.  
  496. document.getElementById('title').value = link.title;
  497. document.getElementById('icon').value = link.icon;
  498. document.getElementById('link').value = link.link;
  499. document.getElementById('searchMethod').value = link.searchMethod;
  500. document.getElementById('group').value = link.group;
  501.  
  502. isEditing = true;
  503. editingIndex = index;
  504. document.getElementById('addLink').textContent = 'Сохранить изменения';
  505. };
  506. window.toggleLink = function(index, enabled) {
  507. let links = getLinks();
  508. links[index].enabled = enabled;
  509. saveLinks(links);
  510. };
  511. function ready(fn) {
  512. document.addEventListener('page:load', fn);
  513. document.addEventListener('turbolinks:load', fn);
  514. if (document.readyState !== "loading") {
  515. fn();
  516. } else {
  517. document.addEventListener('DOMContentLoaded', fn);
  518. }
  519. }
  520.  
  521. ready(init);
  522. ready(GUI);
  523. })();