// ==UserScript==
// @name Hitomi - Search & UI Tweaks
// @namespace brazenvoid
// @version 5.3.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Various search filters and user experience enhancers
// @match https://hitomi.la/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @require https://greasyfork.org/scripts/375557-base-brazen-resource/code/Base%20Brazen%20Resource.js?version=1115796
// @require https://greasyfork.org/scripts/416104-brazen-ui-generator/code/Brazen%20UI%20Generator.js?version=1115813
// @require https://greasyfork.org/scripts/418665-brazen-configuration-manager/code/Brazen%20Configuration%20Manager.js?version=1163542
// @require https://greasyfork.org/scripts/429587-brazen-item-attributes-resolver/code/Brazen%20Item%20Attributes%20Resolver.js?version=1139392
// @require https://greasyfork.org/scripts/416105-brazen-base-search-enhancer/code/Brazen%20Base%20Search%20Enhancer.js?version=1163543
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
GM_addStyle(
`#settings-wrapper{font-family:Open Sans;font-size:12px;font-weight:700;line-height:1;background-color:rgb(216, 210, 234);top:5vh;width:320px}
#settings-wrapper button{font-family:Open Sans;font-size:12px;font-weight:700}
#settings-wrapper textarea.form-input{height:25vh;line-height:2;overflow:auto;padding:0 10px;resize:vertical;white-space:pre;width:92%}
#settings-wrapper .font-secondary{color:black}
.bg-brand{background-color:rgb(216, 210, 234)}
.blacklisted{background-color:lightcoral!important}.blacklisted:hover{background-color:indianred!important}
.blacklisted.favourite{background-color:orange!important}.blacklisted.favourite:hover{background-color:darkorange!important}
.favourite{background-color:mediumseagreen!important}.favourite:hover{background-color:forestgreen!important}`)
const IS_GALLERY_PAGE = $('#dl-button').length
const FILTER_GALLERY_TYPES = 'Show Gallery Types'
const FILTER_PAGES = 'Pages'
const FILTER_TAG_BLACKLIST = 'Tag Blacklist'
const FILTER_LANGUAGES = 'Languages'
const ENABLE_BLACKLIST = 'Enable Blacklist'
const HIGHLIGHT_BLACKLISTED_TAGS = 'Highlight Blacklisted Tags'
const OPTION_REMOVE_RELATED_GALLERIES = 'Remove Related Galleries'
const UI_FAVOURITE_TAGS = 'Favourite Tags'
const UI_SHOW_ALL_TAGS = 'Show All Gallery Tags'
class HitomiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
constructor()
{
super({
isUserLoggedIn: false,
itemDeepAnalysisSelector: '',
itemLinkSelector: '',
itemListSelectors: '.gallery-content',
itemNameSelector: 'h1.lillie a',
itemSelectors: '.acg,.anime,.cg,.dj,.manga',
requestDelay: 0,
scriptPrefix: 'hitomi-sui-',
})
this._configurationManager
.addCheckboxesGroup(FILTER_GALLERY_TYPES, [
['Anime', 'anime'],
['Artist CG', 'acg'],
['Doujinshi', 'dj'],
['Game CG', 'cg'],
['Manga', 'manga'],
], 'Show only selected gallery types.')
.addCheckboxesGroup(FILTER_LANGUAGES, [
['N/A', 'not-applicable'],
['Japanese', 'japanese'],
['Chinese', 'chinese'],
['English', 'english'],
['Albanian', 'albanian'],
['Arabic', 'arabic'],
['Bulgarian', 'bulgarian'],
['Catalan', 'catalan'],
['Cebuano', 'cebuano'],
['Czech', 'czech'],
['Danish', 'danish'],
['Dutch', 'dutch'],
['Esperanto', 'esperanto'],
['Estonian', 'estonian'],
['Finnish', 'finnish'],
['French', 'french'],
['German', 'german'],
['Greek', 'greek'],
['Hebrew', 'hebrew'],
['Hungarian', 'hungarian'],
['Indonesian', 'indonesian'],
['Italian', 'italian'],
['Korean', 'korean'],
['Latin', 'latin'],
['Mongolian', 'mongolian'],
['Norwegian', 'norwegian'],
['Persian', 'persian'],
['Polish', 'polish'],
['Portuguese', 'portuguese'],
['Romanian', 'romanian'],
['Russian', 'russian'],
['Slovak', 'slovak'],
['Spanish', 'spanish'],
['Swedish', 'swedish'],
['Tagalog', 'tagalog'],
['Thai', 'thai'],
['Turkish', 'turkish'],
['Ukrainian', 'ukrainian'],
['Unspecified', 'unspecified'],
['Vietnamese', 'vietnamese'],
], 'Select languages to show')
.addFlagField(ENABLE_BLACKLIST, 'Applies the blacklist.')
.addFlagField(HIGHLIGHT_BLACKLISTED_TAGS, 'Highlights blacklisted tags. Only works with a disabled blacklist.')
.addFlagField(OPTION_REMOVE_RELATED_GALLERIES, 'Remove related galleries section from gallery pages.')
.addFlagField(UI_SHOW_ALL_TAGS, 'Show all gallery tags in search results.')
.addRangeField(FILTER_PAGES, 0, Infinity, 'Close gallery pages that don\'t satisfy these page limits. Only works on galleries opened in new tabs.')
.addRulesetField(
FILTER_TAG_BLACKLIST,
10,
'Specify the tags blacklist with one rule on each line. While single tags can be specified, complex multi-tag rules with & (AND) and | (OR) can also be defined.',
null,
null,
(blacklistedTags) => this._optimizeBlacklistRules(blacklistedTags))
.addRulesetField(
UI_FAVOURITE_TAGS,
10,
'Specify favourite tags that get highlighted in search results.',
null,
null,
(favouriteTags) => this._optimizeFavouriteRules(favouriteTags))
this._onFirstHitAfterCompliance = (item) => {
this._highlightFavouriteTags(item)
this._performComplexFlaggedOperation(
HIGHLIGHT_BLACKLISTED_TAGS, () => !this._getConfig(ENABLE_BLACKLIST), () => this._highlightBlacklistedTags(item))
this._performComplexFlaggedOperation(UI_SHOW_ALL_TAGS, () => !IS_GALLERY_PAGE, () => this._showAllTags(item))
}
this._setupUI()
this._setupComplianceFilters()
}
/**
* @private
*/
_checkPageLimits()
{
let range = this._getConfig(FILTER_PAGES)
if (range.minimum > 0 || range.maximum > 0) {
let navPages = $('.simplePagerNav li').length
let pageCount = navPages > 0 ? navPages * 50 : $('.simplePagerPage1').length
if (!Validator.isInRange(pageCount, range.minimum, range.maximum)) {
top.close()
}
}
}
/**
* @param {string} tag
* @return {JQuery.Selector}
* @private
*/
_formatTagSelector(tag)
{
let selector
tag = encodeURIComponent(tag.trim())
if (tag.startsWith('artist%3A')) {
selector = 'a[href="/artist/' + tag.replace('artist%3A', '') + '-all.html"]'
} else if (tag.startsWith('series%3A')) {
selector = 'a[href="/series/' + tag.replace('series%3A', '') + '-all.html"]'
} else {
selector = 'a[href="/tag/' + tag + '-all.html"]'
}
return selector
}
/**
* @param {string[][]} ruleset
* @param {string[]} tags
* @return {string[][]}
* @private
*/
_growBlacklistRuleset(ruleset, tags)
{
let grownRuleset = []
for (let tag of tags) {
for (let rule of ruleset) {
grownRuleset.push([...rule, this._formatTagSelector(tag)])
}
}
return grownRuleset
}
/**
* @param {JQuery} item
* @private
*/
_highlightBlacklistedTags(item)
{
let isBlacklisted
for (let rule of this._configurationManager.getField(FILTER_TAG_BLACKLIST).optimized) {
isBlacklisted = true
for (let tagSelector of rule) {
if (item.find(tagSelector).length === 0) {
isBlacklisted = false
break
}
}
if (isBlacklisted) {
for (let tagSelector of rule) {
item.find(tagSelector).addClass('blacklisted')
}
}
}
}
/**
* @param {JQuery} item
* @private
*/
_highlightFavouriteTags(item)
{
let favouriteTags = this._configurationManager.getField(UI_FAVOURITE_TAGS)
if (favouriteTags.optimized.length) {
for (let tagSelector of favouriteTags.optimized) {
item.find(tagSelector).addClass('favourite')
}
}
}
/**
* @return {Array}
* @private
*/
_optimizeBlacklistRules(blacklistedRules)
{
let orTags, iteratedRuleset
let optimizedRuleset = []
// Translate user defined rules
for (let blacklistedRule of blacklistedRules) {
iteratedRuleset = []
for (let andTag of blacklistedRule.split('&')) {
orTags = andTag.split('|')
if (orTags.length === 1) {
this._updateBlacklistRuleset(iteratedRuleset, andTag)
} else {
if (iteratedRuleset.length) {
iteratedRuleset = this._growBlacklistRuleset(iteratedRuleset, orTags)
} else {
this._updateBlacklistRuleset(iteratedRuleset, orTags)
}
}
}
optimizedRuleset = optimizedRuleset.concat(iteratedRuleset)
}
// Sort rules by complexity
return optimizedRuleset.sort((a, b) => a.length - b.length)
}
/**
* @return {Array}
* @private
*/
_optimizeFavouriteRules(favouriteRules)
{
let rules = []
for (let tag of favouriteRules) {
rules.push(this._formatTagSelector(tag))
}
return rules
}
/**
* @private
*/
_setupComplianceFilters()
{
this._addItemComplianceFilter(FILTER_LANGUAGES, (item, valueKeys) => {
let languageLink = item.find('tr:nth-child(3) > td:nth-child(2) a')
if (languageLink.length) {
languageLink = languageLink.attr('href')
for (let key of valueKeys) {
if (languageLink.includes(key)) {
return true
}
}
return false
}
return valueKeys.includes('not-applicable')
})
this._addItemComplianceFilter(FILTER_GALLERY_TYPES, (item, valueKeys) => {
for (let galleryClass of valueKeys) {
if (item.hasClass(galleryClass)) {
return true
}
}
return false
})
this._addItemComplexComplianceFilter(
FILTER_TAG_BLACKLIST,
(rules) => this._getConfig(ENABLE_BLACKLIST) && rules.length,
(item, blacklistRuleset) => {
let isBlacklisted
for (let rule of blacklistRuleset) {
isBlacklisted = true
for (let tagSelector of rule) {
if (item.find(tagSelector).length === 0) {
isBlacklisted = false
break
}
}
if (isBlacklisted) {
return false
}
}
return true
},
)
}
/**
* @private
*/
_setupUI()
{
this._onBeforeUIBuild = () => {
if (IS_GALLERY_PAGE) {
this._checkPageLimits()
this._performFlaggedOperation(OPTION_REMOVE_RELATED_GALLERIES, () => $('.gallery-content').remove())
let tagsSection = $('.tags')
this._highlightFavouriteTags(tagsSection)
this._performComplexFlaggedOperation(
HIGHLIGHT_BLACKLISTED_TAGS, () => !this._getConfig(ENABLE_BLACKLIST), () => this._highlightBlacklistedTags(tagsSection))
}
}
this._onUIBuild = () =>
this._uiGen.createSettingsSection().append([
this._uiGen.createTabsSection(['Filters', 'Highlights', 'Languages', 'Global', 'Stats'], [
this._uiGen.createTabPanel('Filters', true).append([
this._configurationManager.createElement(FILTER_GALLERY_TYPES),
this._uiGen.createSeparator(),
this._configurationManager.createElement(FILTER_PAGES),
this._uiGen.createSeparator(),
this._configurationManager.createElement(ENABLE_BLACKLIST),
this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
this._uiGen.createSeparator(),
this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
]),
this._uiGen.createTabPanel('Highlights').append([
this._configurationManager.createElement(HIGHLIGHT_BLACKLISTED_TAGS),
this._configurationManager.createElement(UI_FAVOURITE_TAGS),
]),
this._uiGen.createTabPanel('Languages').append([
this._configurationManager.createElement(FILTER_LANGUAGES),
]),
this._uiGen.createTabPanel('Global').append([
this._configurationManager.createElement(UI_SHOW_ALL_TAGS),
this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
this._uiGen.createSeparator(),
this._createSettingsBackupRestoreFormActions(),
]),
this._uiGen.createTabPanel('Stats').append([
this._uiGen.createStatisticsFormGroup(FILTER_GALLERY_TYPES),
this._uiGen.createStatisticsFormGroup(FILTER_LANGUAGES),
this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
this._uiGen.createSeparator(),
this._uiGen.createStatisticsTotalsGroup(),
]),
]),
this._createSettingsFormActions(),
this._uiGen.createSeparator(),
this._uiGen.createStatusSection(),
])
this._onAfterUIBuild = () => {
this._uiGen.getSelectedSection()[0].userScript = this
}
}
/**
* @param {JQuery} item
* @private
*/
_showAllTags(item)
{
let tags = item.find('.relatedtags > ul > li')
let lastTag = tags.last()
if (lastTag.text() === '...') {
lastTag.remove()
tags.find('.hidden-list-item').removeClass('hidden-list-item')
}
}
/**
* @param {string[][]} ruleset
* @param {string|string[]} tagToAdd
* @private
*/
_updateBlacklistRuleset(ruleset, tagToAdd)
{
if (ruleset.length) {
for (let rule of ruleset) {
rule.push(this._formatTagSelector(tagToAdd))
}
} else {
let tags = typeof tagToAdd === 'string' ? [tagToAdd] : tagToAdd
for (let tag of tags) {
ruleset.push([this._formatTagSelector(tag)])
}
}
}
}
(new HitomiSearchAndUITweaks).init()