// ==UserScript==
// @name Hitomi - Search & UI Tweaks
// @namespace brazenvoid
// @version 2.7.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Search Filters & UI Manipulations
// @include https://hitomi.la/*
// @require https://greasyfork.org/scripts/375557-brazenvoid-s-base-resource/code/Brazenvoid's%20Base%20Resource.js?version=658367
// @run-at document-idle
// ==/UserScript==
// Define languages to keep and tags to exclude here
let settings = {
allowedGalleryTypes: [
'artist cg',
'doujinshi',
'game cg',
'manga',
],
allowedLanguages: [
'japanese',
'english'
],
excludedTags: [
'anthology',
'female:daughter',
'female:dickgirls only',
'female:females only',
'female:mother',
'sample',
'male:father',
'male:son',
'male:yaoi',
],
excludedTagGroups: [
['female:loli', 'female:sole female'],
['male:shota', 'male:sole male'],
],
removeRelatedGalleries: true,
showUIAlways: false,
debugLogging: false,
}
// Translating filters into selectors
// -- Translate gallery types
let allowedGallerySelectors = [], allowedGalleryTypeSelector
for (let allowedGalleryType of settings.allowedGalleryTypes) {
switch (allowedGalleryType) {
case 'anime':
allowedGalleryTypeSelector = 'anime'
break
case 'artist cg':
allowedGalleryTypeSelector = 'acg'
break
case 'doujinshi':
allowedGalleryTypeSelector = 'dj'
break
case 'game cg':
allowedGalleryTypeSelector = 'cg'
break
case 'manga':
allowedGalleryTypeSelector = 'manga'
break
default:
continue
}
allowedGallerySelectors.push(allowedGalleryTypeSelector)
}
// -- Translate tag filters
function encodeURIComponentRFC3986(str) {
return encodeURIComponent(str).replace(/[-!'()*]/g, function(c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
let formatFilters = function (filters, prefix, suffix, join) {
let formatFilter = function (filter) {
return '[href="' + prefix + encodeURIComponentRFC3986(filter) + suffix + '.html"]'
}
let index2
for (let index = 0; index < filters.length; index++) {
if (Array.isArray(filters[index])) {
for (index2 = 0; index2 < filters[index].length; index2++) {
filters[index][index2] = formatFilter(filters[index][index2])
}
} else {
filters[index] = formatFilter(filters[index])
}
}
return join ? filters.join(', ') : filters
}
let languageFiltersSelector = formatFilters(settings.allowedLanguages, '/index-', '-1', true)
let tagFiltersSelector = formatFilters(settings.excludedTags, '/tag/', '-all-1', true)
let tagGroupFiltersSelectors = formatFilters(settings.excludedTagGroups, '/tag/', '-all-1', false)
// Base Resources Initialization
const scriptPrefix = 'hitomi-sui-'
let storage = new LocalStore(scriptPrefix + 'settings', settings)
settings = storage.retrieve().get()
let logger = new Logger(settings.debugLogging)
let selectorGenerator = new SelectorGenerator(scriptPrefix)
let statistics = new StatisticsRecorder(logger, selectorGenerator)
let uiGenerator = new UIGenerator(settings.showUIAlways, selectorGenerator)
// Local Store Events
let refreshUI = function () {
let store = this.get()
document.getElementById(selectorGenerator.getSettingsInputSelector('Min Duration')).value = store.duration.minimum
document.getElementById(selectorGenerator.getSettingsInputSelector('Min Rating')).value = store.rating.minimum
document.getElementById(selectorGenerator.getSettingsInputSelector('Min Views')).value = store.views.minimum
}
storage.onDefaultsLoaded = refreshUI
storage.onRetrieval = refreshUI
storage.onUpdated = refreshUI
// Filtration logic
let validateLanguage = function (gallery) {
let validationCheck = true
if (settings.allowedLanguages.length > 0) {
let languageTD = gallery.querySelector('tr:nth-child(3) > td:nth-child(2)')
if (languageTD.querySelector('a') !== null) {
validationCheck = languageTD.querySelectorAll(languageFiltersSelector).length > 0
}
}
return validationCheck
}
let validateTags = function (gallery) {
let validationCheck = true
if (settings.excludedTags.length > 0) {
validationCheck = gallery.querySelectorAll(tagFiltersSelector).length === 0
statistics.record('Excluded Tags', validationCheck)
}
if (validationCheck && settings.excludedTagGroups.length > 0) {
for (let tagGroupFilterSelectors of tagGroupFiltersSelectors) {
validationCheck = gallery.querySelectorAll(tagGroupFilterSelectors.join(', ')).length < tagGroupFilterSelectors.length
console.log(tagGroupFilterSelectors.join(', ') + ' = ' + validationCheck);
if (!validationCheck) {
break
}
}
statistics.record('Excluded Tag Groups', validationCheck)
}
return validationCheck
}
let validateType = function (gallery) {
let validationCheck = allowedGallerySelectors.includes(gallery.className);
statistics.record('Excluded Tags', validationCheck)
return validationCheck
}
let complianceCallback = function (target) {
let galleries = target.querySelectorAll('.anime, .manga, .dj, .acg, .cg')
let validationCheck
for (let gallery of galleries) {
validationCheck = validateType(gallery) && validateLanguage(gallery) && validateTags(gallery)
if (!validationCheck) {
gallery.remove()
}
}
}
// Script Run
let galleriesList = document.querySelector('.gallery-content')
let isGalleryPage = document.getElementById('dl-button') !== null
if (isGalleryPage && settings.removeRelatedGalleries) {
galleriesList.remove()
} else {
let observer = new ChildObserver(complianceCallback)
observer.observe(galleriesList, true)
}