// ==UserScript==
// @name E-Hentai - UX Tweaks
// @namespace brazenvoid
// @version 1.5.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/1392660/Brazen%20UI%20Generator.js
// @require https://update.greasyfork.org/scripts/418665/1408619/Brazen%20Configuration%20Manager.js
// @require https://update.greasyfork.org/scripts/429587/1244644/Brazen%20Item%20Attributes%20Resolver.js
// @require https://update.greasyfork.org/scripts/416105/1384192/Brazen%20Base%20Search%20Enhancer.js
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
GM_addStyle(
`#settings-wrapper{min-width:310px;width:310px}.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 ITEM_WATCHED = 'watched'
const FILTER_HIDE_WATCHED_FROM_SEARCH = 'Hide Watched Galleries in Search'
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_AUTO_NEXT_ON_OPEN_ALL_IMAGES = 'Auto Next Page';
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';
class EHentaiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
constructor()
{
super({
isUserLoggedIn: false,
itemDeepAnalysisSelector: '',
itemLinkSelector: 'td.gl3m.glname > a, td.gl3c.glname > a, div.gl2e > div > a, a',
itemListSelectors: 'table.itg, div.itg',
itemNameSelector: 'td.gl3m.glname > a > div.glink, td.gl3c.glname > a > div.glink, div.gl4e.glname > div.glink, div.gl4t.glname.glink',
itemSelectors: 'td.gl2e, div.gl1t',
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 {{}} 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_spf', 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
}
}
}
}
/**
* @private
*/
_handleOpenGalleryImages()
{
let images = $('div.gdtl a > img, div.gdtm a > img')
let firstPage = images.first().attr('alt')
let firstPageNumber = Number(firstPage)
let paddedPageLength = firstPage.length
let maxPages = firstPageNumber + images.length - 1
for (let page = maxPages; page >= firstPageNumber; page--) {
let paddedPage = page.toString().padStart(paddedPageLength, '0')
window.open(images.filter('[alt="' + paddedPage + '"]').parent().attr('href'))
}
if (this._getConfig(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES)) {
let page = window.location.href.split('=')[1] || 0
let pageNavs = $('.ptt td')
let 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._onUIBuild(() => {
this._performOperation(UI_VISITED_HIGHLIGHT, () => {
GM_addStyle(`td.gl2e > div > a:visited > .glname > .glink {color: black;}`)
})
if (IS_SEARCH_PAGE) {
this._handleDefaults()
}
})
this._onUIBuilt(() => {
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 = (item) => {
if (item.is('td.gl2e')) {
item.parent().removeClass('noncompliant-item')
item.parent().show()
} else {
item.removeClass('noncompliant-item')
item.show()
}
}
}
/**
* @private
*/
_setupFeatures()
{
this._configurationManager.
addFlagField(
FILTER_HIDE_WATCHED_FROM_SEARCH, 'Hides watched galleries from searches initiated other than the watched page.').
addFlagField(
UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES, '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.').
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.').
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.')
this._itemAttributesResolver.addAttribute(ITEM_WATCHED, (item) => item.find('.gt[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)
this._addItemTagHighlights(
UI_DISLIKED_TAGS, otherTagSections, 'disliked-tag', 'Specify disliked tags to highlight.', 10)
this._addItemTagBlacklistFilter(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', '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('Global').append([
this._configurationManager.createElement(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES),
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()
}