您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
manage your favorite tags, enhance searching, improve comic page
// ==UserScript== // @name ExHentai Viewer // @namespace Violentmonkey Scripts // @description manage your favorite tags, enhance searching, improve comic page // @description:zh-CN 管理标签,增强搜索,改进漫画页面 // @match *://exhentai.org/* // @match *://e-hentai.org/* // @grant GM_setValue // @grant GM_getValue // @version 1.2.0 // ==/UserScript== /* * To-do: * 1. preload images? */ const currentVersion = '1.2.0'; const sortFilters = false; const $ = selector => document.querySelector(selector); const $$ = selector => document.querySelectorAll(selector); const create = tag => document.createElement(tag); let data = { custom_filter: GM_getValue('custom_filter', {}), script_config: GM_getValue('script_config', {}), tag_pref: GM_getValue('tag_pref', { liked_tags: [], disliked_tags: [] }) }; init(); function init() { // upgrade and load script config loadScriptData(); // router const url = window.location.href; if (url.includes('/s/')) { // comic page EHViewer('s'); } else if (url.includes('/mpv')) { // multi page mode EHViewer('mpv'); } else if (url.includes('/g/')) { // gallery page handleGallery(); } else if ($('#searchbox')) { // add panel below the searchbox handleSearchBox(); } else if (url.includes('/gallerytorrents.php')) { // show magnet link showMagnetLink(); } // add EHV setting button let ehvSettingBtn = create('div'); ehvSettingBtn.innerHTML = '<a href="#">EHV Settings</a>'; ehvSettingBtn.onclick = createSettingPanel; $('#nb').append(ehvSettingBtn); $('#nb').style.maxWidth = 'max-content'; // highlight tags highlightTags(); } function loadScriptData() { switch (data.script_config.current_version) { // don't break, just go through the flow to upgrade script data by version case undefined: // just install default: data.script_config.current_version = currentVersion; GM_setValue('custom_filter', data.custom_filter); GM_setValue('script_config', data.script_config); } } function changeStyle(css, mode, id = 'ehv-style') { let cssEl = $('#' + id); if (!cssEl) { cssEl = create('style'); cssEl.type = 'text/css'; cssEl.id = id; document.head.append(cssEl); } switch (mode) { case 'add': cssEl.innerHTML += css; break; case 'replace': cssEl.innerHTML = css; break; } } function addBtnContainer() { let btnStyle = ` #ehv-btn-c{ text-align: center; list-style: none; position: fixed; bottom: 30px; right: 30px; z-index: 999; } .ehv-btn { line-height: 32px; font-size: 16px; padding: 2px; margin: 5px; color: #233; position: relative; width: 32px; height: 32px; border: none; border-radius: 50%; box-shadow: 0 0 3px 0 #0006; cursor: pointer; user-select: none; outline: none; background-color: #fff; background-repeat: no-repeat; background-position: center; } .ehv-btn:hover{ box-shadow: 0 0 3px 1px #0005!important; } .ehv-btn:active { top: 1px; box-shadow: 0 0 1px 1px #0004!important; }`; let btnContainer = create('ul'); btnContainer.id = 'ehv-btn-c'; document.body.append(btnContainer); changeStyle(btnStyle, 'add', 'ehv-btn-c-style'); return btnContainer; } function addBtn(btn, container) { let btnEl = create('li'); if (btn.icon.startsWith('data:image')) { btnEl.style.backgroundImage = "url('" + btn.icon + "')"; } else { btnEl.innerHTML = btn.icon; } btnEl.classList.add('ehv-btn'); btnEl.addEventListener(btn.event, btn.cb); container.append(btnEl); return btnEl; } function EHViewer(mode) { const host = window.location.host; const svgIcons = { autofit: '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>', 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>', zoomOut: '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>', prevPage: '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>', nextPage: '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>', 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>', gallery: '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>' }; let css = ''; let btnContainer = addBtnContainer(); let currentScale = 1; let autofitEnable = true; switch (mode) { case 's': css += ` #i1{ width:98%!important; max-width:98%!important; min-width:800px; background-color: inherit!important; border: none!important } #img{ max-width:none!important; max-height:none!important; background-color: #4f535b!important; padding: 8px; border: 1px solid #000; border-radius: 2px; } h1, #i2, #i5, #i6, #i7, .ip, .sn{ display:none!important; } ::-webkit-scrollbar{ display:none; } html{ scrollbar-width: none; }`; // add btns [ { icon: svgIcons.autofit, event: 'mousedown', cb: autofit }, { icon: svgIcons.zoomIn, event: 'mousedown', cb: e => zoomer(zoomInS) }, { icon: svgIcons.zoomOut, event: 'mousedown', cb: e => zoomer(zoomOutS) }, { icon: svgIcons.prevPage, event: 'click', cb: prevPage }, { icon: svgIcons.nextPage, event: 'click', cb: nextPage }, { icon: svgIcons.reload, event: 'click', cb: e => $('#loadfail').click() }, { icon: svgIcons.gallery, event: 'click', cb: e => $('div.sb>a').click() } ].forEach(v => addBtn(v, btnContainer)); // handle key events document.body.addEventListener('keydown', e => { switch (e.key) { case '=': zoomInS(0.1); break; case '-': zoomOutS(0.1); break; case ',': window.scroll(0, 0); break; case '.': window.scroll(0, window.innerHeight); break; case '[': window.scrollBy(0, -window.innerHeight * 0.3); break; case ']': window.scrollBy(0, window.innerHeight * 0.3); break; } }); // set scale on page load setNewPage(); // set scale on img load let observer = new MutationObserver(setNewPage); observer.observe($('#i3'), { attributes: false, childList: true, subtree: false }); break; case 'mpv': css += '#pane_images_inner>div{margin:auto;}'; // add btns [ { icon: svgIcons.zoomIn, event: 'mousedown', cb: e => zoomer(zoomInMpv) }, { icon: svgIcons.zoomOut, event: 'mousedown', cb: e => zoomer(zoomOutMpv) } ].forEach(v => addBtn(v, btnContainer)); document.body.addEventListener('keydown', e => { switch (e.key) { case '=': zoomInMpv(0.1); break; case '-': zoomOutMpv(0.1); break; } }); break; } // add style if (data.script_config.hide_button) { css += ` #ehv-btn-c{ padding: 80px 30px 30px 80px; bottom: 0!important; right: -80px!important; transition-duration: 300ms; } #ehv-btn-c:hover{ padding: 80px 30px 30px 20px!important; right: 0!important; }`; } css += '.ehv-btn{background-color: #44454B!important;}'; if (host === 'e-hentai.org') { // btn color css = css.replaceAll('#44454B', '#D3D0D1'); // page bg & border (for mode 's') css = css.replace('#4f535b', '#EDEBDF').replace('1px solid #000', '1px solid #5C0D12'); } changeStyle(css, 'replace', 'ehv-style'); // === functions === function autofit() { autofitEnable = true; const img = $('#img'); const imgRatio = img.height / img.width; const windowRatio = window.innerHeight / window.innerWidth; if (imgRatio > windowRatio) { // img thinner than window currentScale = (window.innerHeight - 25) / img.height; } else { //img wider than window currentScale = (window.innerWidth - 25) / img.width; } window.scrollTo(0, 0); zoom4S(); } function zoomer(cb) { // long press to trigger continuous scaling(zoomTimeout > zoomInterval) let zoomInterval = -1; let zoomTimeout = setTimeout(function () { zoomInterval = setInterval(cb, 100); }, 800); document.addEventListener( 'mouseup', e => { if (zoomInterval === -1) { cb(0.1); clearTimeout(zoomTimeout); } else { clearInterval(zoomInterval); } }, { once: true } ); } function prevPage() { const currentPage = $('.sn>span:first-child').innerText; currentPage !== '1' ? $('#prev').click() : alert('This is first page (⊙_⊙)'); } function nextPage() { const currentPage = $('.sn span:first-child').innerText; const lastPage = $('.sn span:last-child').innerText; currentPage !== lastPage ? $('#next').click() : alert('This is last page (⊙ω⊙)'); } function setNewPage() { // add page indicator let footer = $('#i4 > div:first-child'); const currentPage = $('.sn span:first-child').innerText; const totalPage = $('.sn span:last-child').innerText; footer.innerHTML = currentPage + 'P / ' + totalPage + 'P :: ' + footer.innerText + ' :: '; // add origin source let imgLink = $('#i7>a'); imgLink ? footer.append(imgLink) : (footer.innerHTML += 'No download'); if (autofitEnable) { autofit(); } else { // inherit the zoom scale of previous page zoom4S(); } } function zoomInS(pace = 0.02) { autofitEnable = false; currentScale = 1 + pace; zoom4S(); } function zoomOutS(pace = 0.02) { autofitEnable = false; const img = $('#img'); currentScale = 1 - pace; zoom4S(); } function zoom4S() { const img = $('#img'); // img.style.width = currentScale * 100 + '%'; img.style.width = img.width * currentScale + 'px'; img.style.height = 'auto'; } function zoomInMpv(pace = 0.02) { const maxWidth = Number($('#pane_images').style.width.replace('px', '')) - 20; const originalWidth = Number($('#image_1').style.maxWidth.replace('px', '')); currentScale = originalWidth * (1 + pace) < maxWidth ? currentScale + pace : currentScale; zoom4Mpv(originalWidth * currentScale); } function zoomOutMpv(pace = 0.02) { const minWidth = 200; const originalWidth = Number($('#image_1').style.maxWidth.replace('px', '')); currentScale = originalWidth * (currentScale - pace) > minWidth ? currentScale - pace : currentScale; zoom4Mpv(originalWidth * currentScale); } function zoom4Mpv(width) { let mpvStyle = 'img[id^="imgsrc"], div[id^="image"]{width:mpvWidth!important;height:auto!important; max-width:100%!important;min-width:200px!important}"'; mpvStyle = mpvStyle.replace('mpvWidth', width + 'px'); changeStyle(mpvStyle, 'replace', 'custom-width'); } } function handleGallery() { // add searchbox let searchBox = create('form'); searchBox.innerHTML = ` <p class="nopm"> <input type="text" id="f_search" name="f_search" placeholder="Search Keywords" value="" size="60"> <input type="submit" name="f_apply" value="Search"> </p>`; searchBox.setAttribute( 'style', 'display: none; width: 720px; margin: 10px auto; border: 2px ridge black; padding: 10px;' ); searchBox.setAttribute('action', 'https://' + window.location.host + '/'); searchBox.setAttribute('method', 'get'); $('.gm').before(searchBox); // add btn to show/hide searchbox let tbody = $('#taglist > table > tbody'); tbody.innerHTML += ` <tr> <td class="tc">EHV:</td> <td> <div id="ehv-panel-btn" class="gt" style="cursor:pointer">show panel</div> </td> </tr>`; $('#ehv-panel-btn').addEventListener('click', e => { const t = e.target; if (t.innerText == 'show panel') { searchBox.style.display = 'block'; t.innerText = 'hide panel'; } else { searchBox.style.display = 'none'; t.innerText = 'show panel'; } }); // add panel setPanel(); // add gallery tag to searchbox by right-click tbody.addEventListener('contextmenu', applyGalleryTag); function applyGalleryTag(e) { const t = e.target; if (t.tagName === 'A') { e.preventDefault(); const searchInput = $('#f_search'); let tag = t.href.split('/').pop().replaceAll('+', ' '); filter = `"${tag}" `; // add tailing space if (searchInput.value.length > 0 && searchInput.value.endsWith(' ') === false) searchInput.value += ' '; searchInput.value.includes(filter) ? (searchInput.value = searchInput.value.replace(filter, '')) : (searchInput.value += filter); } } } function handleSearchBox() { setPanel(); const ehvPanel = $('#ehv-panel'); if (data.script_config.hide_panel) { ehvPanel.style.display = 'none'; const a = create('a'); a.id = 'ehv-panel-btn'; a.innerText = '[Show EHV Panel]'; a.setAttribute('href', '#'); a.setAttribute('style', 'margin-left: 1em;'); a.addEventListener('click', function () { if (ehvPanel.style.display == 'none') { ehvPanel.style.display = 'block'; a.innerText = '[Hide EHV Panel]'; } else { ehvPanel.style.display = 'none'; a.innerText = '[Show EHV Panel]'; } }); $$('#searchbox>form>div')[1].append(a); } else { $('#ehv-panel-btn') && $('#ehv-panel-btn').remove(); } } function setPanel() { const container = $('#f_search').parentNode; const searchInput = $('#f_search'); // set style const panelCss = `#ehv-panel > input[type="button"]{ margin: 2px; }`; let panelStyle = $('#panel-style'); if (!panelStyle) { panelStyle = create('style'); panelStyle.id = 'panel-style'; panelStyle.textContent = panelCss; document.head.append(panelStyle); } let ehvPanel = $('#ehv-panel'); if (ehvPanel) ehvPanel.remove(); ehvPanel = document.createElement('div'); ehvPanel.setAttribute('class', 'nopm'); ehvPanel.setAttribute('id', 'ehv-panel'); ehvPanel.setAttribute('style', 'padding-top:8px;'); container.append(ehvPanel); // filter buttons for (let key in data.custom_filter) { let filterBtn = create('input'); filterBtn.setAttribute('type', 'button'); filterBtn.setAttribute('value', key); filterBtn.setAttribute('title', data.custom_filter[key].toString()); filterBtn.addEventListener('click', applyFilter); filterBtn.addEventListener('contextmenu', removeFilter); if (isExist(data.custom_filter[key])) { filterBtn.setAttribute('style', 'filter: invert(20%);'); } else { filterBtn.removeAttribute('style'); } ehvPanel.append(filterBtn); } //new filter button let addFilterBtn = create('input'); addFilterBtn.setAttribute('type', 'button'); addFilterBtn.setAttribute('value', '+'); addFilterBtn.addEventListener('click', addFilter); addFilterBtn.addEventListener('contextmenu', ehvSetting); ehvPanel.append(addFilterBtn); // enhace apply filter button const searchBtn = $('#f_search+input'); searchBtn.addEventListener('contextmenu', newtabSearch); searchBtn.title = 'right click to search in new page'; // === function === function isExist(values) { const inputValue = searchInput.value; return values.every(v => inputValue.includes(v)); } function applyFilter(e) { let t = e.target; let key = t.value; let values = data.custom_filter[key]; // add tailing space if (searchInput.value.length > 0 && searchInput.value.endsWith(' ') === false) searchInput.value += ' '; if (isExist(values)) { values.forEach(v => (searchInput.value = searchInput.value.replaceAll(`"${v}" `, ''))); t.removeAttribute('style'); } else { values.forEach(v => (searchInput.value += `"${v}" `)); t.setAttribute('style', 'filter: invert(20%);'); } } function addFilter() { data.custom_filter = GM_getValue('custom_filter', {}); // get latest filter data const input = prompt('Add filter like format below', '[tag] or [name@tag] or [name@tag+tag+tag+tag]'); const filterStr = input ? input.split('@') : ''; let key, value; if (filterStr.length === 2) { key = filterStr[0]; value = filterStr[1].split('+'); data.custom_filter[key] = value; } else if (filterStr.length === 1 && filterStr[0] !== '') { key = value = filterStr[0]; data.custom_filter[key] = [value]; } else { alert('Invalid input...:('); return; } // sort filters by char codepoint if (sortFilters === true) { const newFilters = {}; const sortedKeys = Object.keys(data.custom_filter).sort(); for (let key of sortedKeys) { newFilters[key] = data.custom_filter[key]; } data.custom_filter = newFilters; } GM_setValue('custom_filter', data.custom_filter); setPanel(); } function ehvSetting(e) { e.preventDefault(); data.script_config = GM_getValue('script_config', {}); // sync latest config data data.script_config.hide_panel = confirm('hide EHV panel by default?'); data.script_config.hide_button = confirm( 'hide comic page buttons by default? (hover lower right corner to show)' ); GM_setValue('script_config', data.script_config); handleSearchBox(); } function removeFilter(e) { e.preventDefault(); data.custom_filter = GM_getValue('custom_filter', {}); // get latest filter data if (confirm('Delete this tag?')) { let key = e.target.value; delete data.custom_filter[key]; GM_setValue('custom_filter', data.custom_filter); setPanel(); } } function newtabSearch(e) { e.preventDefault(); const keyword = e.target.previousElementSibling.value; const url = `https://${location.host}/?f_search=${keyword}`; open(url); } } function showMagnetLink() { const links = $$('a'); for (let link of links) { if (link.href.endsWith('.torrent')) { let magnetLink = create('a'); let torrentHash = link.href.match(/[\w\d]{40}/)[0]; magnetLink.href = `magnet:?xt=urn:btih:${torrentHash}&dn=${link.innerText}`; magnetLink.innerText = '[MAGNET]'; link.before(magnetLink); let span = create('span'); span.innerText = ' '; link.before(span); } } } function createSettingPanel() { // todo: // custom filter editor // hide ehv search panel by default // hide ehv sidebar buttons by default console.log('creating ehv setting panel'); let container = create('div'); container.innerHTML = ` <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;"> <span style="color:#233; text-align:left; font-size:12px;">liked tags:</span> <textarea id="ehv-liked-tags" style="padding: .5em;" placeholder="enter tags you like here, separated by comma"></textarea> <span style="color:#233; text-align:left; font-size:12px;">disliked tags:</span> <textarea id="ehv-disliked-tags" style="padding: .5em;" placeholder="enter tags you don't like here, separated by comma"></textarea> <div style="text-align: right;"> <button id="ehv-save-btn">save</button> <button id="ehv-cancel-btn">cancel</button> <div> </div>`; container.setAttribute( 'style', 'background-color: transparent; width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 9999' ); const likedTagsInput = container.querySelector('#ehv-liked-tags'); const dislikedTagsInput = container.querySelector('#ehv-disliked-tags'); const saveBtn = container.querySelector('#ehv-save-btn'); const cancelBtn = container.querySelector('#ehv-cancel-btn'); // get latest tag_pref value data.tag_pref = GM_getValue('tag_pref', { liked_tags: [], disliked_tags: [] }); likedTagsInput.value = data.tag_pref.liked_tags.join(','); dislikedTagsInput.value = data.tag_pref.disliked_tags.join(','); saveBtn.onclick = () => { data.tag_pref.liked_tags = likedTagsInput.value.split(','); data.tag_pref.disliked_tags = dislikedTagsInput.value.split(','); highlightTags(data); GM_setValue('tag_pref', data.tag_pref); container.remove(); }; cancelBtn.onclick = () => container.remove(); document.body.append(container); } function highlightTags() { // highlight tags let tagEls = $$('.gt, .gtl'); const likedTags = data.tag_pref.liked_tags; const dislikedTags = data.tag_pref.disliked_tags; for (let tagEl of tagEls) { let tagStr = tagEl.firstChild.textContent; if (likedTags.includes(tagStr)) { tagEl.style.backgroundColor = '#CCE8'; tagEl.class; } else if (dislikedTags.includes(tagStr)) { tagEl.style.backgroundColor = '#2338'; } else { tagEl.style.backgroundColor = ''; } } }