Tsumino Tweaks

Offline tag search support, nhentai/Hentai2Read links on a book information page and click popup disabling

  1. // ==UserScript==
  2. // @name Tsumino Tweaks
  3. // @description Offline tag search support, nhentai/Hentai2Read links on a book information page and click popup disabling
  4. // @namespace xspeed.net
  5. // @license MIT
  6. // @version 8
  7. // @icon https://cdn.discordapp.com/icons/167128230908657664/b2089ee1d26a7e168d63960d6ed31b66.png
  8. // @match *://www.tsumino.com/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_xmlhttpRequest
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16. // https://gist.github.com/IceCreamYou/8396172
  17. function distDamerauLevenshtein(source, target) {
  18. if (!source) return target ? target.length : 0;
  19. else if (!target) return source.length;
  20.  
  21. var m = source.length, n = target.length, INF = m+n, score = new Array(m+2), sd = {};
  22. for (var i = 0; i < m+2; i++) score[i] = new Array(n+2);
  23. score[0][0] = INF;
  24. for (var i = 0; i <= m; i++) {
  25. score[i+1][1] = i;
  26. score[i+1][0] = INF;
  27. sd[source[i]] = 0;
  28. }
  29. for (var j = 0; j <= n; j++) {
  30. score[1][j+1] = j;
  31. score[0][j+1] = INF;
  32. sd[target[j]] = 0;
  33. }
  34.  
  35. for (var i = 1; i <= m; i++) {
  36. var DB = 0;
  37. for (var j = 1; j <= n; j++) {
  38. var i1 = sd[target[j-1]],
  39. j1 = DB;
  40. if (source[i-1] === target[j-1]) {
  41. score[i+1][j+1] = score[i][j];
  42. DB = j;
  43. }
  44. else {
  45. score[i+1][j+1] = Math.min(score[i][j], Math.min(score[i+1][j], score[i][j+1])) + 1;
  46. }
  47. score[i+1][j+1] = Math.min(score[i+1][j+1], score[i1] ? score[i1][j1] + (i-i1-1) + 1 + (j-j1-1) : Infinity);
  48. }
  49. sd[source[i-1]] = i;
  50. }
  51. return score[m+1][n+1];
  52. }
  53. var cleanTitle = str => str.replace(/\[.*?\]/g, '').replace(/\(.*?\)/g, '').trim();
  54. var jsonError = data => alert(JSON.stringify(data));
  55. unsafeWindow.open = function() {
  56. console.error('Blocked window.open', Array.prototype.slice.apply(arguments));
  57. return { }
  58. };
  59.  
  60. unsafeWindow.showModalDialog = function() {
  61. console.error('Blocked window.showModalDialog', Array.prototype.slice.apply(arguments));
  62. return { }
  63. };
  64. String.prototype.removeAfter = function(char) {
  65. var ix = this.indexOf(char);
  66. return ix == -1 ? this : this.substring(0, ix);
  67. }
  68. if (location.href.indexOf('/entry/') != -1) {
  69.  
  70. var title = $('#Title').text().removeAfter('/').removeAfter('|').trim();
  71. var artist = $('a[data-type="Artist"]').text().trim().removeAfter('|').removeAfter('\n').trim();
  72.  
  73. $('#backToIndex').remove();
  74. $('#btnMakeAccount').remove();
  75. $('#downloadBtnBlocked').remove();
  76.  
  77. var createButton = function(btnText, btnTitle, linkUrl) {
  78. var btn = $('<a href="' + linkUrl + '" id="btnReadNH" class="book-read-button button-stack"><i class="fa fa-arrow-circle-right"></i> ' + btnText + '</a>').insertAfter('#btnReadOnline');
  79. btn.mouseover(function() {
  80. btn.tooltip({
  81. trigger: 'focus',
  82. delay: {
  83. "show": 500,
  84. "hide": 100
  85. },
  86. html: true,
  87. title: btnTitle
  88. });
  89. btn.focus();
  90. });
  91. btn.mouseout(() => btn.blur());
  92. }
  93. var onNH = function(resp) {
  94. var respDoc = $(resp.responseText);
  95. var cover = respDoc.find('.cover');
  96. if (cover && cover.attr('href')) {
  97. cover = cover.map((i, elem) => ({ link: $(elem).attr('href'), title: cleanTitle($(elem).find('.caption').text()), score: distDamerauLevenshtein(title, cleanTitle($(elem).find('.caption').text())) }));
  98. cover.sort((x, y) => x.score - y.score);
  99. createButton('nhentai', cover[0].title, 'https://nhentai.net' + cover[0].link + '1/');
  100. }
  101. }
  102.  
  103. var onH2R = function(resp) {
  104. var respJson = JSON.parse(resp.responseText);
  105. var suggestions = respJson.response.suggestions;
  106. if (suggestions.length > 0) {
  107. createButton('Hentai2Read', suggestions[0].value, suggestions[0].slug + '1/');
  108. }
  109. }
  110.  
  111. GM_xmlhttpRequest({
  112. method: "GET",
  113. url: 'https://nhentai.net/search/?q=english+' + artist.replace(' ', '+') + '+' + title.replace(/[^a-z0-9+]+/gi, '+'),
  114. onload: onNH,
  115. onerror: jsonError
  116. });
  117.  
  118. GM_xmlhttpRequest({
  119. method: "POST",
  120. url: 'https://hentai2read.com/api',
  121. data: 'controller=search&action=all&query=' + encodeURIComponent(title.replace(/\s+#?\d\s*$/, '')),
  122. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  123. onload: onH2R,
  124. onerror: jsonError
  125. });
  126. }
  127.  
  128. else {
  129.  
  130. var tagSearch = $('#tagDataSearch');
  131. if (tagSearch.length) {
  132.  
  133. var tagMode = false;
  134. var tagData = GM_getValue('tagData', '[]');
  135. var tagList = JSON.parse(tagData);
  136. var oldAdapter = null;
  137.  
  138. var offlineQuery = function(params, callback) {
  139. var res = [];
  140.  
  141. if (params && params.term && params.term.length != 0) {
  142. var term = params.term.toLowerCase();
  143.  
  144. for (var tag of tagList) {
  145. if (tag[0].toLowerCase().indexOf(term) != -1) {
  146. res.push(tag[0]);
  147. if (res.length > 4) break;
  148. }
  149. }
  150. }
  151.  
  152. callback({ results: res.map(x => ({ key: 0, text: x, id: x })) });
  153. };
  154.  
  155. tagSearch.next().html('<button id="tagsRefresh" type="button" class="book-read-button" style="padding: 5px 10px; margin: 5px 0;">Refresh tag list</button> Loaded tags: <span id=tagsCount>0</span>')
  156.  
  157. $('#tagsCount').text(Object.keys(tagList).length);
  158.  
  159. $('#selTagType').change(function() {
  160.  
  161. var selData = $('#selTagValue').data('select2');
  162.  
  163. if ($(this).val() == 1 && !tagMode) {
  164. oldAdapter = selData.dataAdapter.query;
  165. tagMode = true;
  166. selData.dataAdapter.query = offlineQuery;
  167. }
  168. else if (tagMode) {
  169. selData.dataAdapter.query = oldAdapter;
  170. tagMode = false;
  171. }
  172.  
  173. });
  174.  
  175. $('#btnSearch').one('click', function() {
  176. $('#selTagType').change();
  177. });
  178.  
  179. $('#tagsRefresh').click(function() {
  180.  
  181. this.disabled = true;
  182. $('#tagsCount').text('0...');
  183.  
  184. var onload = function(resp) {
  185. var respDoc = JSON.parse(resp.responseText);
  186. tagList = [];
  187.  
  188. for (var i = 0; i < respDoc.length; ++i) {
  189. if (respDoc[i].type == "Tag") {
  190. var id = respDoc[i].text.trim();
  191. if (id.length > 0) tagList.push([id, 1]);
  192. }
  193. }
  194.  
  195. $('#tagsCount').text(Object.keys(tagList).length);
  196. GM_setValue('tagData', JSON.stringify(tagList));
  197.  
  198. $('#tagsRefresh').prop('disabled', false);
  199.  
  200. };
  201.  
  202. GM_xmlhttpRequest({ method: "GET", url: 'https://www.tsumino.com/api/Tag/GetAllDefinableTags', onload: onload, onerror: jsonError });
  203.  
  204. });
  205. }
  206. }
  207. })();