// ==UserScript==
// @name XHamster Search and UI Improvements
// @namespace brazenvoid
// @version 2.1.1
// @author brazenvoid
// @license GPL-3.0-only
// @description Various search filters
// @include https://xhamster.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @require https://greasyfork.org/scripts/375557-base-resource/code/Base%20Resource.js?version=899286
// @require https://greasyfork.org/scripts/416104-brazen-ui-generator/code/Brazen%20UI%20Generator.js?version=899448
// @require https://greasyfork.org/scripts/418665-brazen-configuration-manager/code/Brazen%20Configuration%20Manager.js?version=892799
// @require https://greasyfork.org/scripts/416105-brazen-base-search-enhancer/code/Brazen%20Base%20Search%20Enhancer.js?version=899428
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
GM_addStyle(
`#settings-wrapper{top:5vh;width:270px}#settings-wrapper .check-radio-input{display:block}#settings-wrapper .form-button{border:revert}.font-primary{color:white}.tab-button:hover:not(.active){color:black}.tab-button:not(.active){color:white}.bg-brand{background-color:#3a3a3a}`)
const PAGE_PATH_NAME = window.location.pathname
const IS_PROFILE_TREE_PAGE = PAGE_PATH_NAME.startsWith('/users')
const IS_PROFILE_VIDEOS_PAGE = IS_PROFILE_TREE_PAGE && PAGE_PATH_NAME.includes('/videos')
const IS_PROFILE_PAGE = IS_PROFILE_TREE_PAGE && !IS_PROFILE_VIDEOS_PAGE
const IS_VIDEO_PAGE = PAGE_PATH_NAME.startsWith('/videos')
const FILTER_HD_VIDEOS = 'Show only HD Videos'
const FILTER_VIDEOS_VIEWS = 'Views'
const FILTER_WATCHED_VIDEOS = 'Hide Watched Videos'
const SCRIPT_PREFIX = 'xh-sui-'
const SELECTOR_ITEM = '.thumb-list__item'
const SELECTOR_ITEM_LIST = 'div.thumb-list'
const SELECTOR_ITEM_NAME = 'a.video-thumb-info__name'
class XHamsterSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
constructor ()
{
super(SCRIPT_PREFIX, SELECTOR_ITEM)
this._configurationManager.
addFlagField(FILTER_HD_VIDEOS, 'Hides videos of less than 720p resolution.').
addFlagField(FILTER_WATCHED_VIDEOS, 'Hides names of seen videos to aid in ignoring them. Requires page reload to apply.').
addRangeField(FILTER_VIDEOS_VIEWS, 0, 10000000, 'Filter videos by view count.')
if (!IS_VIDEO_PAGE) {
let paginationSection = $('.pager-container ul')
let nextPageNode = paginationSection.find('li.next')
let lastPageUrl = nextPageNode.length ? nextPageNode.prev().find('a').attr('href') : window.location.href
this._paginator = BrazenPaginator.create(paginationSection, '.thumb-list:not(.thumb-list--related):first', SELECTOR_ITEM, lastPageUrl).
onGetPageNoFromUrl((url) => {
let lastSegment = parseInt(url.split('/').pop())
return isNaN(lastSegment) ? 1 : lastSegment
}).
onGetPageUrlFromPageNo((newPageNo) => {
let currentUrl = window.location.href
let currentUrlFragments = currentUrl.split('/')
if (newPageNo === 1) {
if (!isNaN(parseInt(currentUrlFragments[currentUrl.length - 1]))) {
currentUrlFragments.pop()
}
} else {
if (isNaN(parseInt(currentUrlFragments[currentUrl.length - 1]))) {
currentUrlFragments.push(newPageNo.toString())
} else {
currentUrlFragments[currentUrl.length - 1] = newPageNo.toString()
}
}
return currentUrlFragments.join('/')
}).
onGetPaginationElementForPageNo((pageNo, paginator) =>
paginator.getPaginationWrapper().
find(pageNo === paginator.getCurrentPageNo() ? 'a.xh-paginator-button.active' : 'a.xh-paginator-button[data-page=' + pageNo + ']').
parent(),
)
}
this._addPaginationConfiguration()
this._setupUI()
this._setupCompliance()
this._setupComplianceFilters()
}
/**
* @private
*/
_applyWatchedFilter ()
{
if (this._configurationManager.getValue(FILTER_WATCHED_VIDEOS)) {
let color
let isDarkThemeActive = $('.theme-section .dark.active').length
if (IS_PROFILE_VIDEOS_PAGE) {
color = isDarkThemeActive ? '#101010' : 'rgb(234, 234, 234)'
} else {
color = isDarkThemeActive ? '#202020' : '#f5f5f5'
}
GM_addStyle('.video-thumb-info__name:visited,.video .link:visited{color:' + color + '}')
}
}
/**
* @private
*/
_setupCompliance ()
{
this._onGetItemLists = () => $(SELECTOR_ITEM_LIST)
this._onGetItemName = (item) => item.find(SELECTOR_ITEM_NAME).text()
this._onFirstHitBeforeCompliance = (item) => {
Validator.sanitizeTextNode(item.find(SELECTOR_ITEM_NAME), this._configurationManager.getFieldOrFail(FILTER_TEXT_SANITIZATION).optimized)
}
}
/**
* @private
*/
_setupComplianceFilters ()
{
this._addItemTextSanitizationFilter(
'Censor video names by substituting offensive phrases. Each rule in separate line with comma separated target phrases. Example Rule: boyfriend=stepson,stepdad')
this._addItemWhitelistFilter('Show videos with specified phrases in their names. Separate the phrases with line breaks.')
this._addItemTextSearchFilter()
this._addItemPercentageRatingRangeFilter('div.rating span.metric-text')
this._addItemDurationRangeFilter('div.thumb-image-container__duration')
this._addItemComplianceFilter(FILTER_VIDEOS_VIEWS, (item, range) => {
let viewsCount = 0
let viewsCountNode = item.find('div.views span.metric-text')
if (viewsCountNode.length) {
viewsCount = parseInt(viewsCountNode.text().replace(',', ''))
if (isNaN(viewsCount)) {
viewsCount = 0
}
}
return Validator.isInRange(viewsCount, range.minimum, range.maximum)
})
this._addItemComplianceFilter(
FILTER_HD_VIDEOS, (item) => !Validator.isChildMissing(item, 'i.thumb-image-container__icon--hd,i.thumb-image-container__icon--uhd'))
this._addItemBlacklistFilter('Hide videos with specified phrases in their names. Separate the phrases with line breaks.')
}
/**
* @private
*/
_setupUI ()
{
this._onBeforeUIBuild = () => {
if (IS_VIDEO_PAGE) {
Validator.sanitizeNodeOfSelector(
'main > div.width-wrap.with-player-container > h1:first', this._configurationManager.getFieldOrFail(FILTER_TEXT_SANITIZATION).optimized)
}
this._applyWatchedFilter()
}
this._onUIBuild = () =>
this._uiGen.createSettingsSection().append([
this._uiGen.createTabsSection(['Filters', 'Text Based', 'Global'], [
this._uiGen.createTabPanel('Filters', true).append([
this._configurationManager.createElement(FILTER_DURATION_RANGE),
this._configurationManager.createElement(FILTER_PERCENTAGE_RATING_RANGE),
this._configurationManager.createElement(FILTER_VIDEOS_VIEWS),
this._uiGen.createBreakSeparator(),
this._configurationManager.createElement(FILTER_HD_VIDEOS),
this._configurationManager.createElement(FILTER_UNRATED),
this._uiGen.createSeparator(),
this._configurationManager.createElement(OPTION_DISABLE_COMPLIANCE_VALIDATION),
]),
this._uiGen.createTabPanel('Text Based').append([
this._configurationManager.createElement(FILTER_TEXT_SEARCH),
this._configurationManager.createElement(FILTER_TEXT_BLACKLIST),
this._configurationManager.createElement(FILTER_TEXT_WHITELIST),
this._configurationManager.createElement(FILTER_TEXT_SANITIZATION),
]),
this._uiGen.createTabPanel('Global').append([
this._configurationManager.createElement(FILTER_WATCHED_VIDEOS),
this._uiGen.createSeparator(),
this._configurationManager.createElement(CONFIG_PAGINATOR_THRESHOLD),
this._configurationManager.createElement(CONFIG_PAGINATOR_LIMIT),
this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
this._uiGen.createSeparator(),
this._createSettingsBackupRestoreFormActions(),
]),
]),
this._createSettingsFormActions(),
this._uiGen.createSeparator(),
this._uiGen.createStatisticsFormGroup(FILTER_TEXT_BLACKLIST),
this._uiGen.createStatisticsFormGroup(FILTER_TEXT_WHITELIST),
this._uiGen.createStatisticsFormGroup(FILTER_DURATION_RANGE),
this._uiGen.createStatisticsFormGroup(FILTER_HD_VIDEOS, 'High Definition'),
this._uiGen.createStatisticsFormGroup(FILTER_TEXT_SEARCH),
this._uiGen.createStatisticsFormGroup(FILTER_PERCENTAGE_RATING_RANGE),
this._uiGen.createStatisticsFormGroup(FILTER_DURATION_RANGE, 'Unrated'),
this._uiGen.createStatisticsFormGroup(FILTER_VIDEOS_VIEWS),
this._uiGen.createSeparator(),
this._uiGen.createStatisticsTotalsGroup(),
this._uiGen.createSeparator(),
this._uiGen.createStatusSection(),
])
this._onAfterUIBuild = () => {
this._uiGen.getSelectedSection()[0].userScript = this
}
}
}
(new XHamsterSearchAndUITweaks).init()