NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 // @downloadURL https://update.greasyfork.org/scripts/474517/E-Hentai%20-%20UX%20Tweaks.user.js // @updateURL https://update.greasyfork.org/scripts/474517/E-Hentai%20-%20UX%20Tweaks.meta.js // ==/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() }