// ==UserScript==
// @name E-Hentai - UX Tweaks
// @namespace brazenvoid
// @version 1.8.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Numerous features to enrich your browsing experience
// @match https://e-hentai.org/*
// @match https://exhentai.org/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @require https://update.greasyfork.org/scripts/375557/1244990/Base%20Brazen%20Resource.js
// @require https://update.greasyfork.org/scripts/416104/1451214/Brazen%20UI%20Generator.js
// @require https://update.greasyfork.org/scripts/418665/1481350/Brazen%20Configuration%20Manager.js
// @require https://update.greasyfork.org/scripts/429587/1244644/Brazen%20Item%20Attributes%20Resolver.js
// @require https://update.greasyfork.org/scripts/416105/1478692/Brazen%20Base%20Search%20Enhancer.js
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
GM_addStyle(
`#settings-wrapper{min-width:310px;width:310px}.bv-section{font-size:1.25rem}.disliked-tag{background-color:lightcoral !important;color:white !important}.disliked-tag:hover{background-color:indianred !important}.disliked-tag > a{color:white !important}.disliked-tag.favourite-tag{background-color:orange !important}.disliked-tag.favourite-tag:hover{background-color:darkorange !important}.favourite-tag{background-color:mediumseagreen !important;color:white !important}.favourite-tag:hover{background-color:forestgreen !important}.favourite-tag > a{color:white !important}`)
const IS_GALLERY_PAGE = $('#gdt').length
const IS_IMAGE_PAGE = window.location.pathname.startsWith('/s/')
const IS_SEARCH_PAGE = $('#f_search').length
const IS_SMALL_WINDOW = $('.stuffbox').length
const IS_TAG_SEARCH_PAGE = window.location.pathname.startsWith('/tag')
const IS_UPLOADER_SEARCH_PAGE = window.location.pathname.startsWith('/uploader')
const IS_WATCHED_PAGE = document.querySelectorAll('.ido > div > p.ip')?.length > 0
const IS_EXTENDED_LAYOUT = IS_SEARCH_PAGE && $('table.itg.glte').length > 0
const IS_MINIMAL_LAYOUT = !IS_EXTENDED_LAYOUT && $('table.itg.gltm').length > 0
const IS_COMPACT_LAYOUT = !IS_MINIMAL_LAYOUT && $('table.itg.gltc').length > 0
const IS_THUMBNAIL_LAYOUT = !IS_COMPACT_LAYOUT && $('div.itg.gld').length > 0
const ITEM_TAGS = 'tags'
const ITEM_WATCHED = 'watched'
const FILTER_HIDE_WATCHED_FROM_SEARCH = 'Watched Galleries in Search'
const STYLE_GALLERY_HIGHLIGHT = 'gallery-highlight'
const UI_DEFAULTS_PAGE_RANGE = 'Page Range'
const UI_DEFAULTS_PAGE_RANGE_ENABLE = 'Enable Page Range Filter'
const UI_DEFAULTS_RATING = 'Rating'
const UI_DEFAULTS_RATING_ENABLE = 'Enable Rating Filter'
const UI_DEFAULTS_TAGS = 'Tags'
const UI_DEFAULTS_TAGS_ENABLE = 'Enable Default Tags'
const UI_OPEN_GALLERY_PAGES_AUTO_NEXT = 'Auto Next Page'
const UI_OPEN_GALLERY_PAGES_CHUNK_SIZE = 'Chunk Size'
const UI_OPEN_GALLERY_PAGES_DELAY = 'Delay'
const UI_DISLIKED_TAGS = 'Disliked Tags'
const UI_FAVOURITE_TAGS = 'Favourite Tags'
const UI_EMBED_TORRENTS = 'Embed Torrent Downloads'
const UI_VISITED_HIGHLIGHT = 'Highlight Visited'
const UI_GALLERY_HIGHLIGHTS = 'Gallery Highlights'
const UI_GALLERY_HIGHLIGHTS_COlOUR = 'Highlight Colour'
let selectorItem = '', selectorItemLink = '', selectorItemList = '', selectorItemName = ''
if (IS_EXTENDED_LAYOUT) {
selectorItem = 'tr'
selectorItemLink = 'div.gl2e > div > a'
selectorItemList = 'table.itg.glte > tbody'
selectorItemName = 'div.gl4e.glname > div.glink'
} else if (IS_COMPACT_LAYOUT) {
selectorItem = 'tr'
selectorItemLink = 'td.gl3c.glname > a'
selectorItemList = 'table.itg.gltc > tbody'
selectorItemName = 'td.gl3c.glname > a > div.glink'
} else if (IS_MINIMAL_LAYOUT) {
selectorItem = 'tr'
selectorItemLink = 'td.gl3m.glname > a'
selectorItemList = 'table.itg.gltm > tbody'
selectorItemName = 'td.gl3m.glname > a > div.glink'
} else if (IS_THUMBNAIL_LAYOUT) {
selectorItem = 'div.gl1t'
selectorItemLink = 'div.gl3t > a'
selectorItemList = 'div.itg.gld'
selectorItemName = 'div.gl4t.glname'
}
class EHentaiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
constructor()
{
super({
isUserLoggedIn: false,
itemDeepAnalysisSelector: 'div.gm',
itemLinkSelector: selectorItemLink,
itemListSelectors: selectorItemList,
itemNameSelector: selectorItemName,
itemSelectors: selectorItem,
itemSelectionMethod: 'children',
requestDelay: 0,
scriptPrefix: 'e-hentai-ux-',
tagSelectorGenerator: (tag) => {
tag = tag.trim()
if (IS_GALLERY_PAGE) {
let tagAttribute = tag.replaceAll(' ', '_')
return 'div[id="td_' + tagAttribute + '"], a[id="ta_' + tagAttribute + '"]'
}
return 'div.gt[title="' + tag + '"], div.gtl[title="' + tag + '"]'
},
})
this._setupFeatures()
this._setupUI()
this._setupEvents()
}
/**
* @param {string} tag
* @return {string}
* @private
*/
_formatTag(tag)
{
if (tag.includes(':') && !tag.includes('"') && (tag.includes(' ') || tag.includes('+'))) {
tag = tag.replace(':', ':"') + '"'
}
return tag
}
/**
*
* @param {JQuery} item
* @return {string[]}
* @private
*/
_gatherItemTags(item)
{
let tags = []
let tagElements = item.find('.gt,.gtl')
if (IS_EXTENDED_LAYOUT) {
tagElements.each((_i, e) => {
tags.push($(e).attr('title'))
})
} else {
tagElements.each((_i, e) => {
let tagID = $(e).find('a').attr('id')
if (tagID.startsWith('ta_')) {
tagID = tagID.replace('ta_', '')
}
if (tagID.startsWith('td_')) {
tagID = tagID.replace('td_', '')
}
tags.push(tagID.replace('_', ' '))
})
}
return tags
}
/**
* @param {{}} range
* @param {URLSearchParams} queryParams
* @private
*/
_handleDefaultPageRangeFilter(range, queryParams)
{
if (range.minimum > 0) {
queryParams.set('f_spf', range.minimum)
}
if (range.maximum > 0) {
queryParams.set('f_spt', range.maximum)
}
}
/**
* @param {string} rating
* @param {URLSearchParams} queryParams
* @private
*/
_handleDefaultRatingsFilter(rating, queryParams)
{
queryParams.set('f_srdd', rating)
}
/**
* @param {string[]} tags
* @param {URLSearchParams} queryParams
* @private
*/
_handleDefaultTags(tags, queryParams)
{
let existingTags = queryParams.get('f_search')
let updatedTags = existingTags
let include = true
for (let tag of tags) {
if (!existingTags.includes(tag)) {
updatedTags += '+' + this._formatTag(tag)
} else {
include = false
break
}
}
if (include) {
queryParams.set('f_search', updatedTags)
}
}
/**
* @private
*/
_handleDefaults()
{
let queryParams = new URLSearchParams(window.location.search)
let existingParams = queryParams.toString()
if (!queryParams.has('next') &&
(this._getConfig(UI_DEFAULTS_PAGE_RANGE_ENABLE) || this._getConfig(UI_DEFAULTS_RATING_ENABLE) ||
this._getConfig(UI_DEFAULTS_TAGS_ENABLE))) {
if (!queryParams.has('f_search')) {
let existingTag = ''
let urlSegments = window.location.pathname.split('/')
if (IS_TAG_SEARCH_PAGE) {
existingTag = urlSegments.pop().trim()
} else if (IS_UPLOADER_SEARCH_PAGE) {
existingTag = 'uploader:' + urlSegments.pop().trim()
}
queryParams.set('f_search', existingTag.length ? this._formatTag(existingTag) : '')
}
if (!queryParams.has('advsearch')) {
queryParams.set('advsearch', '1')
}
let validatePageRange = (range, defaultValidator) => defaultValidator(range) && !queryParams.has('f_spf') &&
!queryParams.has('f_spt')
this._performTogglableComplexOperation(UI_DEFAULTS_PAGE_RANGE_ENABLE, UI_DEFAULTS_PAGE_RANGE, validatePageRange,
(range) => {
this._handleDefaultPageRangeFilter(range, queryParams)
})
let validateRatingFilter = (range, defaultValidator) => defaultValidator(range) && !queryParams.has('f_srdd')
this._performTogglableComplexOperation(UI_DEFAULTS_RATING_ENABLE, UI_DEFAULTS_RATING, validateRatingFilter,
(rating) => {
this._handleDefaultRatingsFilter(rating, queryParams)
})
this._performTogglableOperation(UI_DEFAULTS_TAGS_ENABLE, UI_DEFAULTS_TAGS, (tags) => {
this._handleDefaultTags(tags, queryParams)
})
let updatedParams = queryParams.toString().replaceAll('%2B', '+')
if (updatedParams !== existingParams) {
if (IS_TAG_SEARCH_PAGE || IS_UPLOADER_SEARCH_PAGE) {
window.location = window.location.origin + '?' + updatedParams
} else {
window.location = window.location.origin + window.location.pathname + '?' + updatedParams
}
}
}
}
/**
* @param {JQuery} item
* @private
*/
_handleGalleryHighlights(item)
{
let mode = this._getConfig(UI_GALLERY_HIGHLIGHTS)
let itemHasHighlight = item.hasClass(STYLE_GALLERY_HIGHLIGHT)
if (mode !== 'Disabled') {
let itemTags = this._get(item, ITEM_TAGS), doHighlight, tag, itemHasTag
if (itemTags) {
for (let rule of this._configurationManager.getField(UI_FAVOURITE_TAGS).optimized) {
doHighlight = true
for (let tagSelector of rule) {
tag = tagSelector.split('"], div.gtl[title="').pop().replace('"]', '')
if ((mode === 'All' && !itemTags.includes(tag)) ||
(mode === 'Source' && ((!tag.startsWith('artist:') && !tag.startsWith('group:')) || !itemTags.includes(tag)))) {
doHighlight = false
break
}
}
if (doHighlight) {
if (!itemHasHighlight) {
item.addClass(STYLE_GALLERY_HIGHLIGHT)
}
break
}
}
if (!doHighlight && itemHasHighlight) {
item.removeClass(STYLE_GALLERY_HIGHLIGHT)
}
}
} else if (itemHasHighlight) {
item.removeClass(STYLE_GALLERY_HIGHLIGHT)
}
}
/**
* @private
*/
async _handleOpenGalleryImages()
{
let chunkSize = this._getConfig(UI_OPEN_GALLERY_PAGES_CHUNK_SIZE) + 1
let delay = this._getConfig(UI_OPEN_GALLERY_PAGES_DELAY)
let images = $('#gdt > a')
let iteration = 0
let firstPageNumber = images.first().attr('href').split('-').pop()
let maxPages = firstPageNumber + images.length - 1
for (let page = images.length - 1; page >= 0; page--) {
if (chunkSize && delay) {
iteration++
if (iteration % chunkSize === 0) {
await Utilities.sleep(delay * 1000)
iteration = 0
}
}
window.open(images.eq(page).attr('href'))
}
if (this._getConfig(UI_OPEN_GALLERY_PAGES_AUTO_NEXT)) {
let page = window.location.href.split('=')[1] || 0
let pageNavs = $('.ptt td')
maxPages = Number.parseInt(pageNavs.eq(pageNavs.length - 2).children('a').text()) - 1
if (page < maxPages) {
let uri = window.location.href
if (page === 0) {
uri += '?p=1'
} else {
uri = uri.replace('?p=' + page++, '?p=' + page)
}
window.location = uri
}
}
}
/**
* @private
*/
_handleTorrentDownloadsEmbedding()
{
let link = $('#gd5 > .g2 > a').eq(1)
if (!link.text().endsWith('(0)')) {
let container = $('<div class="gm"></div>').insertBefore('#cdiv')
container.load(link.attr('onclick').replace('return popUp(\'', '').replace('\',610,590)', '') + ' form', () => {
container.prepend('<h1 style="font-size:10pt; font-weight:bold; margin:3px; text-align:center">Torrents</h1>')
link.parent().remove()
})
}
}
/**
* @private
*/
_setupEvents()
{
this._onValidateInit = () => !IS_SMALL_WINDOW
this._onBeforeUIBuild.push(() => {
this._performOperation(UI_VISITED_HIGHLIGHT, () => {
GM_addStyle(`td.gl2e > div > a:visited > .glname > .glink {color: black;}`)
})
if (IS_SEARCH_PAGE) {
this._handleDefaults()
GM_addStyle('.gallery-highlight{background-color:' + this._getConfig(UI_GALLERY_HIGHLIGHTS_COlOUR) + ' !important;border:whitesmoke 2px solid}')
}
})
this._onAfterUIBuild.push(() => {
this._uiGen.getSelectedSection()[0].userScript = this
if (IS_GALLERY_PAGE) {
this._performOperation(UI_EMBED_TORRENTS, () => this._handleTorrentDownloadsEmbedding())
}
})
this._onItemHide = (item) => {
if (item.is('td.gl2e')) {
item.parent().addClass('noncompliant-item')
item.parent().hide()
} else {
item.removeClass('noncompliant-item')
item.hide()
}
}
this._onItemShow.push((item) => {
if (item.is('td.gl2e')) {
item.parent().removeClass('noncompliant-item')
item.parent().show()
} else {
item.removeClass('noncompliant-item')
item.show()
}
})
if (IS_SEARCH_PAGE) {
this._onItemShow.push((item) => this._handleGalleryHighlights(item))
}
}
/**
* @private
*/
_setupFeatures()
{
this._configurationManager.
addFlagField(
FILTER_HIDE_WATCHED_FROM_SEARCH,
'Hides watched galleries from searches initiated other than the watched page.').
addFlagField(
UI_OPEN_GALLERY_PAGES_AUTO_NEXT, 'Automatically navigates to the next page after opening all images.').
addFlagField(
UI_DEFAULTS_PAGE_RANGE_ENABLE,
'Always set these page limits in searches. Ignored if you set your own values on the page.').
addFlagField(
UI_DEFAULTS_RATING_ENABLE, 'Enable default rating filter in searches').
addFlagField(
UI_DEFAULTS_TAGS_ENABLE, 'Enable default tags in searches.').
addFlagField(
UI_EMBED_TORRENTS, 'Embed torrent downloads in gallery pages.').
addFlagField(
UI_VISITED_HIGHLIGHT, 'Colours the visited gallery links black, to make them distinct.').
addNumberField(
UI_OPEN_GALLERY_PAGES_CHUNK_SIZE, 0, 1000, 'Number of pages to open in one go. Set 0 to open all.').
addNumberField(
UI_OPEN_GALLERY_PAGES_DELAY, 0, 60, 'The delay between chunks in seconds. Set 0 to disable.').
addRadiosGroup(
UI_DEFAULTS_RATING,
[
['2 stars', '2'],
['3 stars', '3'],
['4 stars', '4'],
['5 stars', '5'],
],
'Always set this rating filter in searches. Ignored if you set your own value on the page.').
addRadiosGroup(
UI_GALLERY_HIGHLIGHTS,
[
['Disabled', 'Disabled'],
['All Favourite Tags', 'All'],
['Only Group / Artist Tags', 'Source'],
],
'Highlights favourite galleries in search results with at least one matching tag.').
addRangeField(
UI_DEFAULTS_PAGE_RANGE, 0, 2000, 'Enable default page range filter in searches.').
addRulesetField(
UI_DEFAULTS_TAGS,
3,
'Always add the following tags in search. Can be overridden with at least one tag present.').
addTextField(
UI_GALLERY_HIGHLIGHTS_COlOUR, 'Colour to highlight the galleries with. Requires refresh to change.', 'mediumaquamarine')
this._addItemTagAttribute(ITEM_TAGS, !IS_EXTENDED_LAYOUT, false, (item) => this._gatherItemTags(item))
this._itemAttributesResolver.addAttribute(
ITEM_WATCHED,
(item) => item.find('.gt[style],.gtl[style]').length > 0)
let otherTagSections = IS_GALLERY_PAGE ? $('#taglist') : null
this._addItemComplexComplianceFilter(
FILTER_HIDE_WATCHED_FROM_SEARCH,
(enabled) => !IS_GALLERY_PAGE && !IS_WATCHED_PAGE && enabled,
(item) => !this._get(item, ITEM_WATCHED))
this._addItemTagHighlights(
UI_FAVOURITE_TAGS,
otherTagSections,
'favourite-tag',
'Specify favourite tags to highlight.',
10,
'disliked-tag')
this._addItemTagHighlights(
UI_DISLIKED_TAGS,
otherTagSections,
'disliked-tag',
'Specify disliked tags to highlight.',
10,
'favourite-tag')
this._addItemTagBlacklistFilter(ITEM_TAGS, false, 20)
}
/**
* @private
*/
_setupUI()
{
let galleryOptions = []
if (IS_GALLERY_PAGE) {
galleryOptions = [
this._uiGen.createSeparator(),
this._uiGen.createFormButton(
'Open Gallery Images',
'Opens all images on current page of this gallery.',
() => this._handleOpenGalleryImages(),
),
]
}
this._userInterface = [
this._uiGen.createTabsSection(['Filters', 'Highlights', 'Defaults', 'Galleries', 'Global'], [
this._uiGen.createTabPanel('Filters', true).append([
this._configurationManager.createElement(FILTER_HIDE_WATCHED_FROM_SEARCH),
this._configurationManager.createElement(OPTION_ENABLE_TAG_BLACKLIST),
this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
]),
this._uiGen.createTabPanel('Highlights').append([
this._configurationManager.createElement(UI_FAVOURITE_TAGS),
this._configurationManager.createElement(UI_DISLIKED_TAGS),
]),
this._uiGen.createTabPanel('Defaults').append([
this._configurationManager.createElement(UI_DEFAULTS_PAGE_RANGE_ENABLE),
this._configurationManager.createElement(UI_DEFAULTS_PAGE_RANGE),
this._uiGen.createSeparator(),
this._configurationManager.createElement(UI_DEFAULTS_RATING),
this._uiGen.createBreakSeparator(),
this._configurationManager.createElement(UI_DEFAULTS_RATING_ENABLE),
this._uiGen.createSeparator(),
this._configurationManager.createElement(UI_DEFAULTS_TAGS_ENABLE),
this._configurationManager.createElement(UI_DEFAULTS_TAGS),
]),
this._uiGen.createTabPanel('Galleries').append([
this._configurationManager.createElement(UI_OPEN_GALLERY_PAGES_AUTO_NEXT),
this._configurationManager.createElement(UI_OPEN_GALLERY_PAGES_CHUNK_SIZE),
this._configurationManager.createElement(UI_OPEN_GALLERY_PAGES_DELAY),
this._uiGen.createSeparator(),
this._configurationManager.createElement(UI_GALLERY_HIGHLIGHTS),
this._uiGen.createBreakSeparator(),
this._uiGen.createBreakSeparator(),
this._configurationManager.createElement(UI_GALLERY_HIGHLIGHTS_COlOUR),
]),
this._uiGen.createTabPanel('Global').append([
this._configurationManager.createElement(UI_EMBED_TORRENTS),
this._configurationManager.createElement(UI_VISITED_HIGHLIGHT),
this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
this._uiGen.createSeparator(),
this._createSettingsBackupRestoreFormActions(),
]),
]),
(IS_GALLERY_PAGE || IS_WATCHED_PAGE) ? '' : this._uiGen.createStatisticsFormGroup(
FILTER_HIDE_WATCHED_FROM_SEARCH),
IS_GALLERY_PAGE ? '' : this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
...galleryOptions,
this._uiGen.createSeparator(),
this._createSettingsFormActions(),
this._uiGen.createSeparator(),
this._uiGen.createStatusSection(),
]
}
}
if (!IS_IMAGE_PAGE) {
(new EHentaiSearchAndUITweaks).init()
}