// ==UserScript==
// @name ExHentai Viewer
// @namespace Violentmonkey Scripts
// @description manage your favorite tags, enhance searching, improve comic page
// @match *://exhentai.org/*
// @match *://e-hentai.org/*
// @grant GM_setValue
// @grant GM_getValue
// @version 0.53
// ==/UserScript==
const currentVersion = '0.53';
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', {})
}
init();
function init() {
// upgrade user data if not compatible
upgradeScriptData();
// 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();
}
}
function upgradeScriptData() {
if (data.script_config.currentVersion) {
// correct wrong naming style in 0.41
data.script_config.current_version = data.script_config.currentVersion;
delete data.script_config.currentVersion;
}
switch (data.script_config.current_version) {
// don't break, just go through the flow
case undefined:
if (data.custom_filter.length === undefined) {
// just install
} else {
// 0.37 or ealier
// backup old data
data.script_config.old_version_data = [{
'version': '0.37-',
'data': data.custom_filter
}];
// migrate custom filters
let new_custom_filter = {};
for (let item of data.custom_filter) {
if (item && item.name && item.tag) {
new_custom_filter[item.name] = item.tag.split('+');
}
}
data.custom_filter = new_custom_filter;
}
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 = {
zoomIn: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="rgb(120,120,120)" d="M14 7H9V2a1 1 0 0 0-2 0v5H2a1 1 0 0 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2z"></path></svg>',
zoomOut: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect fill="rgb(120,120,120)" x="2" y="7" width="12" height="2" rx="1"></rect></svg>',
prevPage: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="rgb(120,120,120)" d="M15 7H3.414l4.293-4.293a1 1 0 0 0-1.414-1.414l-6 6a1 1 0 0 0 0 1.414l6 6a1 1 0 0 0 1.414-1.414L3.414 9H15a1 1 0 0 0 0-2z"></path></svg>',
nextPage: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="rgb(120,120,120)" d="M15.707 7.293l-6-6a1 1 0 0 0-1.414 1.414L12.586 7H1a1 1 0 0 0 0 2h11.586l-4.293 4.293a1 1 0 1 0 1.414 1.414l6-6a1 1 0 0 0 0-1.414z"></path></svg>',
reload: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="rgb(120,120,120)" d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>',
gallery: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="rgb(120,120,120)"><rect x="1" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="1" width="6" height="6" rx="1" ry="1"></rect><rect x="1" y="9" width="6" height="6" rx="1" ry="1"></rect><rect x="9" y="9" width="6" height="6" rx="1" ry="1"></rect></g></svg>'
}
let css = '';
let btnContainer = addBtnContainer();
let currentScale;
switch (mode) {
case 's':
currentScale = 0.45;
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.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':
currentScale = 1;
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 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';
// inherit the zoom scale of previous page
zoom4S();
}
function zoomInS(pace = 0.02) {
const targetScale = currentScale * (1 + pace);
currentScale = targetScale > 1 ? 1 : targetScale;
zoom4S();
}
function zoomOutS(pace = 0.02) {
const img = $('#img');
const targetScale = currentScale * (1 - pace);
currentScale = img.height > window.innerHeight ? targetScale : currentScale;
zoom4S();
}
function zoom4S() {
const img = $('#img');
img.style.width = currentScale * 100 + '%';
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="Apply Filter">
</p>`;
searchBox.setAttribute('style', 'display: none; width: 600px; 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('+', " ");
tag = `"${tag}"`;
searchInput.value.includes(tag) ? searchInput.value = searchInput.value.replace(tag, '') : searchInput.value += tag;
}
}
}
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';
}
});
$$('p.nopm')[1].append(a);
}else{
$('#ehv-panel-btn') && $('#ehv-panel-btn').remove();
}
}
function setPanel() {
const filters = data.custom_filter;
const container = $('.nopm');
const searchInput = $('#f_search');
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 filters) {
let filterBtn = create('input');
filterBtn.setAttribute('type', 'button');
filterBtn.setAttribute('value', key);
filterBtn.setAttribute('title', filters[key].toString());
filterBtn.addEventListener('click', applyFilter);
filterBtn.addEventListener('contextmenu', removeFilter);
if (isExist(filters[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);
//
// === 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 = filters[key];
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() {
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('+');
filters[key] = value;
}else if(filterStr.length === 1 && filterStr[0] !== ''){
key = value = filterStr[0];
filters[key] = [value];
}else{
alert('Invalid input...:(');
return;
}
GM_setValue('custom_filter', filters);
setPanel();
}
function ehvSetting(e){
e.preventDefault();
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();
if(confirm('Delete this tag?')){
let key = e.target.value;
delete filters[key];
GM_setValue('custom_filter', filters);
setPanel();
}
}
}