ExHentai Viewer

manage your favorite tags, enhance searching, improve comic page

  1. // ==UserScript==
  2. // @name ExHentai Viewer
  3. // @namespace Violentmonkey Scripts
  4. // @description manage your favorite tags, enhance searching, improve comic page
  5. // @description:zh-CN 管理标签,增强搜索,改进漫画页面
  6. // @match *://exhentai.org/*
  7. // @match *://e-hentai.org/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @version 1.2.0
  11. // ==/UserScript==
  12. /*
  13. * To-do:
  14. * 1. preload images?
  15. */
  16.  
  17. const currentVersion = '1.2.0';
  18. const sortFilters = false;
  19. const $ = selector => document.querySelector(selector);
  20. const $$ = selector => document.querySelectorAll(selector);
  21. const create = tag => document.createElement(tag);
  22. let data = {
  23. custom_filter: GM_getValue('custom_filter', {}),
  24. script_config: GM_getValue('script_config', {}),
  25. tag_pref: GM_getValue('tag_pref', { liked_tags: [], disliked_tags: [] })
  26. };
  27.  
  28. init();
  29.  
  30. function init() {
  31. // upgrade and load script config
  32. loadScriptData();
  33. // router
  34. const url = window.location.href;
  35. if (url.includes('/s/')) {
  36. // comic page
  37. EHViewer('s');
  38. } else if (url.includes('/mpv')) {
  39. // multi page mode
  40. EHViewer('mpv');
  41. } else if (url.includes('/g/')) {
  42. // gallery page
  43. handleGallery();
  44. } else if ($('#searchbox')) {
  45. // add panel below the searchbox
  46. handleSearchBox();
  47. } else if (url.includes('/gallerytorrents.php')) {
  48. // show magnet link
  49. showMagnetLink();
  50. }
  51.  
  52. // add EHV setting button
  53. let ehvSettingBtn = create('div');
  54. ehvSettingBtn.innerHTML = '<a href="#">EHV Settings</a>';
  55. ehvSettingBtn.onclick = createSettingPanel;
  56. $('#nb').append(ehvSettingBtn);
  57. $('#nb').style.maxWidth = 'max-content';
  58.  
  59. // highlight tags
  60. highlightTags();
  61. }
  62.  
  63. function loadScriptData() {
  64. switch (data.script_config.current_version) {
  65. // don't break, just go through the flow to upgrade script data by version
  66. case undefined:
  67. // just install
  68. default:
  69. data.script_config.current_version = currentVersion;
  70. GM_setValue('custom_filter', data.custom_filter);
  71. GM_setValue('script_config', data.script_config);
  72. }
  73. }
  74.  
  75. function changeStyle(css, mode, id = 'ehv-style') {
  76. let cssEl = $('#' + id);
  77. if (!cssEl) {
  78. cssEl = create('style');
  79. cssEl.type = 'text/css';
  80. cssEl.id = id;
  81. document.head.append(cssEl);
  82. }
  83. switch (mode) {
  84. case 'add':
  85. cssEl.innerHTML += css;
  86. break;
  87. case 'replace':
  88. cssEl.innerHTML = css;
  89. break;
  90. }
  91. }
  92.  
  93. function addBtnContainer() {
  94. let btnStyle = `
  95. #ehv-btn-c{
  96. text-align: center;
  97. list-style: none;
  98. position: fixed;
  99. bottom: 30px;
  100. right: 30px;
  101. z-index: 999;
  102. }
  103. .ehv-btn {
  104. line-height: 32px;
  105. font-size: 16px;
  106. padding: 2px;
  107. margin: 5px;
  108. color: #233;
  109. position: relative;
  110. width: 32px;
  111. height: 32px;
  112. border: none;
  113. border-radius: 50%;
  114. box-shadow: 0 0 3px 0 #0006;
  115. cursor: pointer;
  116. user-select: none;
  117. outline: none;
  118. background-color: #fff;
  119. background-repeat: no-repeat;
  120. background-position: center;
  121. }
  122. .ehv-btn:hover{
  123. box-shadow: 0 0 3px 1px #0005!important;
  124. }
  125. .ehv-btn:active {
  126. top: 1px;
  127. box-shadow: 0 0 1px 1px #0004!important;
  128. }`;
  129. let btnContainer = create('ul');
  130. btnContainer.id = 'ehv-btn-c';
  131. document.body.append(btnContainer);
  132. changeStyle(btnStyle, 'add', 'ehv-btn-c-style');
  133. return btnContainer;
  134. }
  135.  
  136. function addBtn(btn, container) {
  137. let btnEl = create('li');
  138. if (btn.icon.startsWith('data:image')) {
  139. btnEl.style.backgroundImage = "url('" + btn.icon + "')";
  140. } else {
  141. btnEl.innerHTML = btn.icon;
  142. }
  143. btnEl.classList.add('ehv-btn');
  144. btnEl.addEventListener(btn.event, btn.cb);
  145. container.append(btnEl);
  146. return btnEl;
  147. }
  148.  
  149. function EHViewer(mode) {
  150. const host = window.location.host;
  151. const svgIcons = {
  152. autofit:
  153. 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" fill="rgba(120,120,120,1)"></path></svg>',
  154. zoomIn: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748ZM10 10V7H12V10H15V12H12V15H10V12H7V10H10Z" fill="rgba(120,120,120,1)"></path></svg>',
  155. zoomOut:
  156. 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748ZM7 10H15V12H7V10Z" fill="rgba(120,120,120,1)"></path></svg>',
  157. prevPage:
  158. 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z" fill="rgba(120,120,120,1)"></path></svg>',
  159. nextPage:
  160. 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M13.1714 12.0007L8.22168 7.05093L9.63589 5.63672L15.9999 12.0007L9.63589 18.3646L8.22168 16.9504L13.1714 12.0007Z" fill="rgba(120,120,120,1)"></path></svg>',
  161. reload: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="rgba(120,120,120,1)"></path></svg>',
  162. gallery:
  163. 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M5.82843 6.99955L8.36396 9.53509L6.94975 10.9493L2 5.99955L6.94975 1.0498L8.36396 2.46402L5.82843 4.99955H13C17.4183 4.99955 21 8.58127 21 12.9996C21 17.4178 17.4183 20.9996 13 20.9996H4V18.9996H13C16.3137 18.9996 19 16.3133 19 12.9996C19 9.68584 16.3137 6.99955 13 6.99955H5.82843Z" fill="rgba(120,120,120,1)"></path></svg>'
  164. };
  165. let css = '';
  166. let btnContainer = addBtnContainer();
  167. let currentScale = 1;
  168. let autofitEnable = true;
  169.  
  170. switch (mode) {
  171. case 's':
  172. css += `
  173. #i1{
  174. width:98%!important;
  175. max-width:98%!important;
  176. min-width:800px;
  177. background-color: inherit!important;
  178. border: none!important
  179. }
  180. #img{
  181. max-width:none!important;
  182. max-height:none!important;
  183. background-color: #4f535b!important;
  184. padding: 8px;
  185. border: 1px solid #000;
  186. border-radius: 2px;
  187. }
  188. h1, #i2, #i5, #i6, #i7, .ip, .sn{
  189. display:none!important;
  190. }
  191. ::-webkit-scrollbar{
  192. display:none;
  193. }
  194. html{
  195. scrollbar-width: none;
  196. }`;
  197. // add btns
  198. [
  199. { icon: svgIcons.autofit, event: 'mousedown', cb: autofit },
  200. { icon: svgIcons.zoomIn, event: 'mousedown', cb: e => zoomer(zoomInS) },
  201. { icon: svgIcons.zoomOut, event: 'mousedown', cb: e => zoomer(zoomOutS) },
  202. { icon: svgIcons.prevPage, event: 'click', cb: prevPage },
  203. { icon: svgIcons.nextPage, event: 'click', cb: nextPage },
  204. { icon: svgIcons.reload, event: 'click', cb: e => $('#loadfail').click() },
  205. { icon: svgIcons.gallery, event: 'click', cb: e => $('div.sb>a').click() }
  206. ].forEach(v => addBtn(v, btnContainer));
  207. // handle key events
  208. document.body.addEventListener('keydown', e => {
  209. switch (e.key) {
  210. case '=':
  211. zoomInS(0.1);
  212. break;
  213. case '-':
  214. zoomOutS(0.1);
  215. break;
  216. case ',':
  217. window.scroll(0, 0);
  218. break;
  219. case '.':
  220. window.scroll(0, window.innerHeight);
  221. break;
  222. case '[':
  223. window.scrollBy(0, -window.innerHeight * 0.3);
  224. break;
  225. case ']':
  226. window.scrollBy(0, window.innerHeight * 0.3);
  227. break;
  228. }
  229. });
  230. // set scale on page load
  231. setNewPage();
  232. // set scale on img load
  233. let observer = new MutationObserver(setNewPage);
  234. observer.observe($('#i3'), { attributes: false, childList: true, subtree: false });
  235. break;
  236. case 'mpv':
  237. css += '#pane_images_inner>div{margin:auto;}';
  238. // add btns
  239. [
  240. { icon: svgIcons.zoomIn, event: 'mousedown', cb: e => zoomer(zoomInMpv) },
  241. { icon: svgIcons.zoomOut, event: 'mousedown', cb: e => zoomer(zoomOutMpv) }
  242. ].forEach(v => addBtn(v, btnContainer));
  243. document.body.addEventListener('keydown', e => {
  244. switch (e.key) {
  245. case '=':
  246. zoomInMpv(0.1);
  247. break;
  248. case '-':
  249. zoomOutMpv(0.1);
  250. break;
  251. }
  252. });
  253. break;
  254. }
  255.  
  256. // add style
  257. if (data.script_config.hide_button) {
  258. css += `
  259. #ehv-btn-c{
  260. padding: 80px 30px 30px 80px;
  261. bottom: 0!important;
  262. right: -80px!important;
  263. transition-duration: 300ms;
  264. }
  265. #ehv-btn-c:hover{
  266. padding: 80px 30px 30px 20px!important;
  267. right: 0!important;
  268. }`;
  269. }
  270. css += '.ehv-btn{background-color: #44454B!important;}';
  271. if (host === 'e-hentai.org') {
  272. // btn color
  273. css = css.replaceAll('#44454B', '#D3D0D1');
  274. // page bg & border (for mode 's')
  275. css = css.replace('#4f535b', '#EDEBDF').replace('1px solid #000', '1px solid #5C0D12');
  276. }
  277. changeStyle(css, 'replace', 'ehv-style');
  278.  
  279. // === functions ===
  280. function autofit() {
  281. autofitEnable = true;
  282. const img = $('#img');
  283. const imgRatio = img.height / img.width;
  284. const windowRatio = window.innerHeight / window.innerWidth;
  285. if (imgRatio > windowRatio) {
  286. // img thinner than window
  287. currentScale = (window.innerHeight - 25) / img.height;
  288. } else {
  289. //img wider than window
  290. currentScale = (window.innerWidth - 25) / img.width;
  291. }
  292. window.scrollTo(0, 0);
  293. zoom4S();
  294. }
  295. function zoomer(cb) {
  296. // long press to trigger continuous scaling(zoomTimeout > zoomInterval)
  297. let zoomInterval = -1;
  298. let zoomTimeout = setTimeout(function () {
  299. zoomInterval = setInterval(cb, 100);
  300. }, 800);
  301. document.addEventListener(
  302. 'mouseup',
  303. e => {
  304. if (zoomInterval === -1) {
  305. cb(0.1);
  306. clearTimeout(zoomTimeout);
  307. } else {
  308. clearInterval(zoomInterval);
  309. }
  310. },
  311. { once: true }
  312. );
  313. }
  314. function prevPage() {
  315. const currentPage = $('.sn>span:first-child').innerText;
  316. currentPage !== '1' ? $('#prev').click() : alert('This is first page (⊙_⊙)');
  317. }
  318. function nextPage() {
  319. const currentPage = $('.sn span:first-child').innerText;
  320. const lastPage = $('.sn span:last-child').innerText;
  321. currentPage !== lastPage ? $('#next').click() : alert('This is last page (⊙ω⊙)');
  322. }
  323. function setNewPage() {
  324. // add page indicator
  325. let footer = $('#i4 > div:first-child');
  326. const currentPage = $('.sn span:first-child').innerText;
  327. const totalPage = $('.sn span:last-child').innerText;
  328. footer.innerHTML = currentPage + 'P / ' + totalPage + 'P :: ' + footer.innerText + ' :: ';
  329. // add origin source
  330. let imgLink = $('#i7>a');
  331. imgLink ? footer.append(imgLink) : (footer.innerHTML += 'No download');
  332. if (autofitEnable) {
  333. autofit();
  334. } else {
  335. // inherit the zoom scale of previous page
  336. zoom4S();
  337. }
  338. }
  339. function zoomInS(pace = 0.02) {
  340. autofitEnable = false;
  341. currentScale = 1 + pace;
  342. zoom4S();
  343. }
  344. function zoomOutS(pace = 0.02) {
  345. autofitEnable = false;
  346. const img = $('#img');
  347. currentScale = 1 - pace;
  348. zoom4S();
  349. }
  350. function zoom4S() {
  351. const img = $('#img');
  352. // img.style.width = currentScale * 100 + '%';
  353. img.style.width = img.width * currentScale + 'px';
  354. img.style.height = 'auto';
  355. }
  356. function zoomInMpv(pace = 0.02) {
  357. const maxWidth = Number($('#pane_images').style.width.replace('px', '')) - 20;
  358. const originalWidth = Number($('#image_1').style.maxWidth.replace('px', ''));
  359. currentScale = originalWidth * (1 + pace) < maxWidth ? currentScale + pace : currentScale;
  360. zoom4Mpv(originalWidth * currentScale);
  361. }
  362. function zoomOutMpv(pace = 0.02) {
  363. const minWidth = 200;
  364. const originalWidth = Number($('#image_1').style.maxWidth.replace('px', ''));
  365. currentScale = originalWidth * (currentScale - pace) > minWidth ? currentScale - pace : currentScale;
  366. zoom4Mpv(originalWidth * currentScale);
  367. }
  368. function zoom4Mpv(width) {
  369. let mpvStyle =
  370. 'img[id^="imgsrc"], div[id^="image"]{width:mpvWidth!important;height:auto!important; max-width:100%!important;min-width:200px!important}"';
  371. mpvStyle = mpvStyle.replace('mpvWidth', width + 'px');
  372. changeStyle(mpvStyle, 'replace', 'custom-width');
  373. }
  374. }
  375.  
  376. function handleGallery() {
  377. // add searchbox
  378. let searchBox = create('form');
  379. searchBox.innerHTML = `
  380. <p class="nopm">
  381. <input type="text" id="f_search" name="f_search" placeholder="Search Keywords" value="" size="60">
  382. <input type="submit" name="f_apply" value="Search">
  383. </p>`;
  384. searchBox.setAttribute(
  385. 'style',
  386. 'display: none; width: 720px; margin: 10px auto; border: 2px ridge black; padding: 10px;'
  387. );
  388. searchBox.setAttribute('action', 'https://' + window.location.host + '/');
  389. searchBox.setAttribute('method', 'get');
  390. $('.gm').before(searchBox);
  391.  
  392. // add btn to show/hide searchbox
  393. let tbody = $('#taglist > table > tbody');
  394. tbody.innerHTML += `
  395. <tr>
  396. <td class="tc">EHV:</td>
  397. <td>
  398. <div id="ehv-panel-btn" class="gt" style="cursor:pointer">show panel</div>
  399. </td>
  400. </tr>`;
  401. $('#ehv-panel-btn').addEventListener('click', e => {
  402. const t = e.target;
  403. if (t.innerText == 'show panel') {
  404. searchBox.style.display = 'block';
  405. t.innerText = 'hide panel';
  406. } else {
  407. searchBox.style.display = 'none';
  408. t.innerText = 'show panel';
  409. }
  410. });
  411.  
  412. // add panel
  413. setPanel();
  414.  
  415. // add gallery tag to searchbox by right-click
  416. tbody.addEventListener('contextmenu', applyGalleryTag);
  417.  
  418. function applyGalleryTag(e) {
  419. const t = e.target;
  420. if (t.tagName === 'A') {
  421. e.preventDefault();
  422. const searchInput = $('#f_search');
  423. let tag = t.href.split('/').pop().replaceAll('+', ' ');
  424. filter = `"${tag}" `;
  425. // add tailing space
  426. if (searchInput.value.length > 0 && searchInput.value.endsWith(' ') === false) searchInput.value += ' ';
  427. searchInput.value.includes(filter)
  428. ? (searchInput.value = searchInput.value.replace(filter, ''))
  429. : (searchInput.value += filter);
  430. }
  431. }
  432. }
  433.  
  434. function handleSearchBox() {
  435. setPanel();
  436. const ehvPanel = $('#ehv-panel');
  437. if (data.script_config.hide_panel) {
  438. ehvPanel.style.display = 'none';
  439. const a = create('a');
  440. a.id = 'ehv-panel-btn';
  441. a.innerText = '[Show EHV Panel]';
  442. a.setAttribute('href', '#');
  443. a.setAttribute('style', 'margin-left: 1em;');
  444. a.addEventListener('click', function () {
  445. if (ehvPanel.style.display == 'none') {
  446. ehvPanel.style.display = 'block';
  447. a.innerText = '[Hide EHV Panel]';
  448. } else {
  449. ehvPanel.style.display = 'none';
  450. a.innerText = '[Show EHV Panel]';
  451. }
  452. });
  453. $$('#searchbox>form>div')[1].append(a);
  454. } else {
  455. $('#ehv-panel-btn') && $('#ehv-panel-btn').remove();
  456. }
  457. }
  458.  
  459. function setPanel() {
  460. const container = $('#f_search').parentNode;
  461. const searchInput = $('#f_search');
  462.  
  463. // set style
  464. const panelCss = `#ehv-panel > input[type="button"]{ margin: 2px; }`;
  465. let panelStyle = $('#panel-style');
  466. if (!panelStyle) {
  467. panelStyle = create('style');
  468. panelStyle.id = 'panel-style';
  469. panelStyle.textContent = panelCss;
  470. document.head.append(panelStyle);
  471. }
  472.  
  473. let ehvPanel = $('#ehv-panel');
  474.  
  475. if (ehvPanel) ehvPanel.remove();
  476.  
  477. ehvPanel = document.createElement('div');
  478. ehvPanel.setAttribute('class', 'nopm');
  479. ehvPanel.setAttribute('id', 'ehv-panel');
  480. ehvPanel.setAttribute('style', 'padding-top:8px;');
  481. container.append(ehvPanel);
  482. // filter buttons
  483. for (let key in data.custom_filter) {
  484. let filterBtn = create('input');
  485. filterBtn.setAttribute('type', 'button');
  486. filterBtn.setAttribute('value', key);
  487. filterBtn.setAttribute('title', data.custom_filter[key].toString());
  488. filterBtn.addEventListener('click', applyFilter);
  489. filterBtn.addEventListener('contextmenu', removeFilter);
  490. if (isExist(data.custom_filter[key])) {
  491. filterBtn.setAttribute('style', 'filter: invert(20%);');
  492. } else {
  493. filterBtn.removeAttribute('style');
  494. }
  495. ehvPanel.append(filterBtn);
  496. }
  497. //new filter button
  498. let addFilterBtn = create('input');
  499. addFilterBtn.setAttribute('type', 'button');
  500. addFilterBtn.setAttribute('value', '+');
  501. addFilterBtn.addEventListener('click', addFilter);
  502. addFilterBtn.addEventListener('contextmenu', ehvSetting);
  503. ehvPanel.append(addFilterBtn);
  504.  
  505. // enhace apply filter button
  506. const searchBtn = $('#f_search+input');
  507. searchBtn.addEventListener('contextmenu', newtabSearch);
  508. searchBtn.title = 'right click to search in new page';
  509.  
  510. // === function ===
  511. function isExist(values) {
  512. const inputValue = searchInput.value;
  513. return values.every(v => inputValue.includes(v));
  514. }
  515. function applyFilter(e) {
  516. let t = e.target;
  517. let key = t.value;
  518. let values = data.custom_filter[key];
  519.  
  520. // add tailing space
  521. if (searchInput.value.length > 0 && searchInput.value.endsWith(' ') === false) searchInput.value += ' ';
  522.  
  523. if (isExist(values)) {
  524. values.forEach(v => (searchInput.value = searchInput.value.replaceAll(`"${v}" `, '')));
  525. t.removeAttribute('style');
  526. } else {
  527. values.forEach(v => (searchInput.value += `"${v}" `));
  528. t.setAttribute('style', 'filter: invert(20%);');
  529. }
  530. }
  531. function addFilter() {
  532. data.custom_filter = GM_getValue('custom_filter', {}); // get latest filter data
  533. const input = prompt('Add filter like format below', '[tag] or [name@tag] or [name@tag+tag+tag+tag]');
  534. const filterStr = input ? input.split('@') : '';
  535. let key, value;
  536. if (filterStr.length === 2) {
  537. key = filterStr[0];
  538. value = filterStr[1].split('+');
  539. data.custom_filter[key] = value;
  540. } else if (filterStr.length === 1 && filterStr[0] !== '') {
  541. key = value = filterStr[0];
  542. data.custom_filter[key] = [value];
  543. } else {
  544. alert('Invalid input...:(');
  545. return;
  546. }
  547. // sort filters by char codepoint
  548. if (sortFilters === true) {
  549. const newFilters = {};
  550. const sortedKeys = Object.keys(data.custom_filter).sort();
  551. for (let key of sortedKeys) {
  552. newFilters[key] = data.custom_filter[key];
  553. }
  554. data.custom_filter = newFilters;
  555. }
  556. GM_setValue('custom_filter', data.custom_filter);
  557. setPanel();
  558. }
  559. function ehvSetting(e) {
  560. e.preventDefault();
  561. data.script_config = GM_getValue('script_config', {}); // sync latest config data
  562. data.script_config.hide_panel = confirm('hide EHV panel by default?');
  563. data.script_config.hide_button = confirm(
  564. 'hide comic page buttons by default? (hover lower right corner to show)'
  565. );
  566. GM_setValue('script_config', data.script_config);
  567. handleSearchBox();
  568. }
  569. function removeFilter(e) {
  570. e.preventDefault();
  571. data.custom_filter = GM_getValue('custom_filter', {}); // get latest filter data
  572. if (confirm('Delete this tag?')) {
  573. let key = e.target.value;
  574. delete data.custom_filter[key];
  575. GM_setValue('custom_filter', data.custom_filter);
  576. setPanel();
  577. }
  578. }
  579. function newtabSearch(e) {
  580. e.preventDefault();
  581. const keyword = e.target.previousElementSibling.value;
  582. const url = `https://${location.host}/?f_search=${keyword}`;
  583. open(url);
  584. }
  585. }
  586.  
  587. function showMagnetLink() {
  588. const links = $$('a');
  589. for (let link of links) {
  590. if (link.href.endsWith('.torrent')) {
  591. let magnetLink = create('a');
  592. let torrentHash = link.href.match(/[\w\d]{40}/)[0];
  593. magnetLink.href = `magnet:?xt=urn:btih:${torrentHash}&dn=${link.innerText}`;
  594. magnetLink.innerText = '[MAGNET]';
  595. link.before(magnetLink);
  596. let span = create('span');
  597. span.innerText = ' ';
  598. link.before(span);
  599. }
  600. }
  601. }
  602.  
  603. function createSettingPanel() {
  604. // todo:
  605. // custom filter editor
  606. // hide ehv search panel by default
  607. // hide ehv sidebar buttons by default
  608. console.log('creating ehv setting panel');
  609. let container = create('div');
  610. container.innerHTML = `
  611. <div id="ehv-setting-panel" style="display: flex; flex-direction: column; gap:.5rem; background-color: #CCCE; border-radius: 3px; box-shadow: 0 0 3px 0 #0008; margin: 5rem auto; padding: 1rem; max-width: 50rem;">
  612. <span style="color:#233; text-align:left; font-size:12px;">liked tags:</span>
  613. <textarea id="ehv-liked-tags" style="padding: .5em;" placeholder="enter tags you like here, separated by comma"></textarea>
  614. <span style="color:#233; text-align:left; font-size:12px;">disliked tags:</span>
  615. <textarea id="ehv-disliked-tags" style="padding: .5em;" placeholder="enter tags you don't like here, separated by comma"></textarea>
  616. <div style="text-align: right;">
  617. <button id="ehv-save-btn">save</button>
  618. <button id="ehv-cancel-btn">cancel</button>
  619. <div>
  620. </div>`;
  621. container.setAttribute(
  622. 'style',
  623. 'background-color: transparent; width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 9999'
  624. );
  625.  
  626. const likedTagsInput = container.querySelector('#ehv-liked-tags');
  627. const dislikedTagsInput = container.querySelector('#ehv-disliked-tags');
  628. const saveBtn = container.querySelector('#ehv-save-btn');
  629. const cancelBtn = container.querySelector('#ehv-cancel-btn');
  630.  
  631. // get latest tag_pref value
  632. data.tag_pref = GM_getValue('tag_pref', { liked_tags: [], disliked_tags: [] });
  633.  
  634. likedTagsInput.value = data.tag_pref.liked_tags.join(',');
  635. dislikedTagsInput.value = data.tag_pref.disliked_tags.join(',');
  636.  
  637. saveBtn.onclick = () => {
  638. data.tag_pref.liked_tags = likedTagsInput.value.split(',');
  639. data.tag_pref.disliked_tags = dislikedTagsInput.value.split(',');
  640. highlightTags(data);
  641. GM_setValue('tag_pref', data.tag_pref);
  642. container.remove();
  643. };
  644. cancelBtn.onclick = () => container.remove();
  645.  
  646. document.body.append(container);
  647. }
  648.  
  649. function highlightTags() {
  650. // highlight tags
  651. let tagEls = $$('.gt, .gtl');
  652. const likedTags = data.tag_pref.liked_tags;
  653. const dislikedTags = data.tag_pref.disliked_tags;
  654.  
  655. for (let tagEl of tagEls) {
  656. let tagStr = tagEl.firstChild.textContent;
  657. if (likedTags.includes(tagStr)) {
  658. tagEl.style.backgroundColor = '#CCE8';
  659. tagEl.class;
  660. } else if (dislikedTags.includes(tagStr)) {
  661. tagEl.style.backgroundColor = '#2338';
  662. } else {
  663. tagEl.style.backgroundColor = '';
  664. }
  665. }
  666. }