Skip to content

Instantly share code, notes, and snippets.

@TankNut
Last active April 21, 2024 21:18
Show Gist options
  • Save TankNut/1a3b21335044a9cecfcfa603b99a8855 to your computer and use it in GitHub Desktop.
Save TankNut/1a3b21335044a9cecfcfa603b99a8855 to your computer and use it in GitHub Desktop.
Adds some missing functionality back to the hub and brings in new features.
// ==UserScript==
// @name HypnoHub Userscript
// @namespace https://github.com/TankNut
// @version 2.6.5
// @description Adds some missing functionality back to the hub
// @author TankNut
// @updateURL https://gist.githubusercontent.com/TankNut/1a3b21335044a9cecfcfa603b99a8855/raw/hypnohub-userscript.user.js
// @downloadURL https://gist.githubusercontent.com/TankNut/1a3b21335044a9cecfcfa603b99a8855/raw/hypnohub-userscript.user.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js
// @require https://userscripts-mirror.org/scripts/source/107941.user.js
// @match https://hypnohub.net/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=hypnohub.net
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect https://hypnohub.net
// ==/UserScript==
// eslint-disable-next-line no-unused-vars
const implications = `
after_sex after_anal after_vaginal
ass hypnotic_ass ass_expansion ass_grab large_ass huge_ass cum_in_ass cum_on_ass
bikini micro_bikini
bottomless nude
breast_grab groping holding_breasts
breasts small_breasts large_breasts huge_breasts hyper_breasts hypnotic_breasts cum_on_breasts breast_expansion breast_sucking breast_grab
collaboration collaborative_fellatio collaborative_paizuri collaborative_handjob collaborative_buttjob collaborative_footjob collaborative_breast_smother
cum cum_in_pussy cum_on_body cum_in_mouth cum_on_face cum_on_breasts cum_in_ass cum_on_hair cum_in_uterus cum_on_clothes cum_in_clothing covered_with_cum cum_on_ass cumming_out_brain cum_drinking cum_in_nose hypnotic_cum cum_on_feet cum_on_hands cum_in_nipples cum_in_ear bukkake
dress dress_lift
feet hypnotic_feet barefoot foot_focus
fellatio collaborative_fellatio double_fellatio fellatio_under_mask autofellatio
footjob collaborative_footjob
futanari futasub futadom futa_only multiple_futa
gloves fingerless_gloves opera_gloves
glowing glowing_eyes
handjob collaborative_handjob double_handjob
magic hypnotic_magic
manip caption
oral fellatio analingus cunnilingus
paizuri double_paizuri collaborative_paizuri
penis small_penis large_penis huge_cock hyper_cock
piercing nipple_piercing navel_piercing tongue_piercing clitoris_piercing penis_piercing nose_piercing lip_piercing nose_ring
pov male_pov female_pov pov_dom pov_sub
robot robot_girl robot_boy robotization
sex vaginal anal missionary doggy_style
sex_toy dildo vibrator anal_beads
skirt skirt_lift
sound voice_acted
standing standing_at_attention
symbol_in_eyes binary_eyes heart_eyes spiral_eyes
tentacles tentacle_sex tentaclejob hypnotic_tentacle
text caption caption_only translated partially_translated translation_request dialogue
tongue tongue_out
topless nude
urination golden_shower
`;
/* eslint no-unused-vars: 0 */
const j = $.noConflict(true);
j.fn.random = function() {
return j(this[Math.floor(Math.random() * this.length)]);
};
j.fn.center = function(options) {
options = j.extend({ // Default values
inside:window, // element, center into window
transition: 0, // millisecond, transition time
minX:0, // pixel, minimum left element value
minY:0, // pixel, minimum top element value
withScrolling:false, // booleen, take care of the scrollbar (scrollTop)
vertical:true, // booleen, center vertical
horizontal:true // booleen, center horizontal
}, options);
return this.each(function() {
const props = {position:'fixed'};
if (options.vertical) {
let top = (j(options.inside).height() - j(this).outerHeight()) / 2;
if (options.withScrolling) {
top += j(options.inside).scrollTop() || 0;
}
top = (top > options.minY ? top : options.minY);
j.extend(props, {top: top+'px'});
}
if (options.horizontal) {
let left = (j(options.inside).width() - j(this).outerWidth()) / 2;
if (options.withScrolling) {
left += j(options.inside).scrollLeft() || 0;
}
left = (left > options.minX ? left : options.minX);
j.extend(props, {left: left+'px'});
}
if (options.transition > 0) {
j(this).animate(props, options.transition);
} else {
j(this).css(props);
}
return j(this);
});
}
function getUrlParameter(param, staticUrl) {
const url = new URL(staticUrl ? staticUrl : window.location, window.location);
return url.searchParams.get(param) ?? false;
}
function promisedRequest(details) {
return new Promise((resolve, reject) => {
details.onabort = reject;
details.onerror = reject;
details.ontimeout = reject;
details.onload = resolve;
GM_xmlhttpRequest(details);
});
}
function getRenderedSize(contains, cWidth, cHeight, width, height, pos) {
var oRatio = width / height,
cRatio = cWidth / cHeight;
return function() {
if (contains ? (oRatio > cRatio) : (oRatio < cRatio)) {
this.width = cWidth;
this.height = cWidth / oRatio;
} else {
this.width = cHeight * oRatio;
this.height = cHeight;
}
this.left = (cWidth - this.width)*(pos/100);
this.right = this.width + this.left;
return this;
}.call({});
}
function getImgSizeInfo(img) {
var pos = window.getComputedStyle(img).getPropertyValue('object-position').split(' ');
return getRenderedSize(true,
img.width, img.height,
img.naturalWidth, img.naturalHeight,
parseInt(pos[0])
);
}
function getCookie(key) {
const match = document.cookie.match(new RegExp('(^| )' + key + '=([^;]+)'));
if (match) {
return match[2];
}
}
// eslint-disable-next-line no-unused-vars
class API {
static get(options) {
const url = new URL('https://hypnohub.net/index.php');
options = {
page: 'dapi',
s: 'post',
json: 1,
...options
}
for (const [option, val] of Object.entries(options)) {
url.searchParams.set(option, val)
}
return promisedRequest({
url: url.href,
responseType: options.json == 1 ? 'json' : 'text'
});
}
static getPost(id, force) {
return new Promise((resolve) => {
const cache = Cache.get('post_' + id);
if (cache && !force) {
resolve(cache);
} else {
resolve(this.get({
s: 'post',
q: 'index',
id: id
}).then(details => {
if (!details.response) {
return false;
}
const data = details.response[0];
Cache.set('post_' + id, {
width: data.width,
height: data.height,
file_url: data.file_url
}, moment.duration(1, 'months'));
return data;
}));
}
});
}
static getSearch(tags, page) {
return this.get({
s: 'post',
q: 'index',
tags: tags,
pid: page,
limit: 42
}).then(details => details.response);
}
static editPost(id, ops) {
return this.get({
page: 'post',
s: 'view',
id: id,
json: 0
}).then(details => {
return new Promise(resolve => {
const doc = new DOMParser().parseFromString(details.response, 'text/html');
const data = j('#edit_form', doc).serializeArray();
const get = name => data.find(obj => obj.name == name).value;
const set = (name, val) => data.find(obj => obj.name == name).value = val.toString();
set('pconf', 1);
if (ops.addTags.length > 0 || ops.removeTags.length > 0) {
const tags = Object.fromEntries(get('tags').split(' ').filter(tag => tag.length > 0).map(tag => [tag, true]));
ops.addTags?.forEach(tag => tags[tag] = true);
ops.removeTags?.forEach(tag => delete tags[tag]);
set('tags', Object.keys(tags).sort().join(' '));
}
if(ops.implications == 'builtin' || ops.implications == 'both') set('tags', Implications.handle(get('tags'), implications));
if(ops.implications == 'custom' || ops.implications == 'both') set('tags', Implications.handle(get('tags'), Settings.get('tag_implications')));
if(ops.rating) set('rating', ops.rating);
if(ops.parent) set('parent', ops.parent);
if(ops.source) set('source', ops.source);
j.post('./public/edit_post.php', data, resolve);
});
});
}
static getQueue() {
const url = new URL('https://hypnohub.net/admin/moderate/');
url.searchParams.set('page', 'post_queue');
return promisedRequest({
url: url.href,
responseType: 'text'
}).then(details => {
const doc = new DOMParser().parseFromString(details.response, 'text/html');
const count = j('table.highlightable > tbody > tr', doc).length - 1;
return count;
});
}
}
// eslint-disable-next-line no-unused-vars
class Cache {
static regex = /cache_.+/;
static init() {
if (!this.get('initial-setup-done')) {
let imported = false;
for (const key of GM_listValues()) {
if (Settings.settings[key]) {
imported = true;
Settings.set(key, GM_SuperValue.get(key, Settings.settings[key].fallback));
}
}
if (imported) {
Cache.set('version', '1.18.0');
Notice.add('Your old settings have been successfully transferred over.', {dismissable: true});
}
this.set('initial-setup-done', true);
}
const now = this.time();
for (const key of GM_listValues()) {
if (key.match(this.regex)) {
const container = GM_SuperValue.get(key);
if (typeof container === 'undefined')
continue;
if (container.expire) {
if (typeof container.expire == 'string') {
GM_deleteValue(key);
} else if (container.lastAccess + container.expire < now) {
GM_deleteValue(key);
}
}
} else {
GM_deleteValue(key);
}
}
}
static time(time) {
return Math.floor(+(time ?? moment()) / 1000);
}
static get(key, fallback) {
key = 'cache_' + key;
const container = GM_SuperValue.get(key);
if (typeof container === 'undefined') {
return fallback;
}
if (container.expire) {
container.lastAccess = this.time();
GM_SuperValue.set(key, container);
}
return container.data;
}
static has(key) {
return typeof GM_SuperValue.get('cache_' + key) !== 'undefined';
}
static set(key, data, expire) {
const container = {
data: data
}
if (expire) {
container.lastAccess = this.time();
container.expire = this.time(expire);
}
GM_SuperValue.set('cache_' + key, container);
}
static delete(key) {
GM_deleteValue('cache_' + key);
}
}
// eslint-disable-next-line no-unused-vars
class ClickDrag {
static init(element) {
const w = j(window);
element.css('cursor', 'grab').attr('draggable', false);
let pos = { x: 0, y: 0 };
const mouseDownHandler = function(e) {
if (e.which != 1) return;
element.css('cursor', 'grabbing');
pos = {
x: e.clientX,
y: e.clientY
};
w.mousemove(mouseMoveHandler);
w.mouseup(mouseUpHandler);
}
const mouseMoveHandler = function(e) {
window.scrollBy(pos.x - e.clientX, pos.y - e.clientY);
pos = {
x: e.clientX,
y: e.clientY
};
}
const mouseUpHandler = function() {
w.off('mousemove', mouseMoveHandler);
w.off('mouseup', mouseUpHandler);
element.css('cursor', 'grab');
}
element.mousedown(mouseDownHandler);
}
}
// eslint-disable-next-line no-unused-vars
class Favorite {
}
// eslint-disable-next-line no-unused-vars
class Filter {
static parse(list) {
return list.split('\n').filter(group => group.length > 0)
.map(element => element.split(' ').filter(tag => tag.length > 0));
}
static escape(string) {
return string.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
static colorRegExp = new RegExp('^#[\\da-f]+$', 'i');
static match(tags, list) {
let found = false;
list.some(group => {
let color = '#ff9800';
const subgroup = [...group];
if (this.colorRegExp.test(group[0])) {
color = group[0];
subgroup.shift();
}
if (subgroup.every(tag => {
const inverted = (tag[0] == '~' || tag[0] == '!');
if (inverted) {
return !(new RegExp(`(\\s|^)${this.escape(tag.substring(1))}(\\s|$)`, 'g').test(tags));
} else {
return new RegExp(`(\\s|^)${this.escape(tag)}(\\s|$)`, 'g').test(tags);
}
})) {
found = color
return true;
}
});
return found;
}
static applyGreylist() {
let greylist = Settings.get('greylist');
if (Settings.get('greylist_videos')) {
greylist += '\n#00f video';
}
if (Settings.get('greylist_gifs')) {
greylist += '\n#0ff animated_gif';
}
greylist = this.parse(greylist);
const filter = this;
j('.thumb img').each(function() {
const element = j(this);
const tags = element.attr('title');
const match = filter.match(tags, greylist);
if (match) {
element.css('border', `3px solid ${match}`);
} else {
element.css('border', '');
}
})
}
}
// eslint-disable-next-line no-unused-vars
class Implications {
static init() {
const setting = Settings.get('tag_implications_mode');
j('#edit_form > table > tbody > tr:nth-child(14)').before(`<tr><td>
Tag Implications:
<select id="implications">
<option ${setting == 'off' ? 'selected' : ''}>Off</option>
<option ${setting == 'builtin' ? 'selected' : ''}>Built-in</option>
<option ${setting == 'custom' ? 'selected' : ''}>Custom</option>
<option ${setting == 'both' ? 'selected' : ''}>Both</option>
</select></td></tr>`);
j('#edit_form').submit(() => {
const mode = j('#implications').prop('selectedIndex');
if (mode == 0) return;
let tags = j('#tags').val();
if (mode == 1 || mode == 3) tags = this.handle(tags, implications);
if (mode == 2 || mode == 3) tags = this.handle(tags, Settings.get('tag_implications'));
j('#tags').val(tags);
});
}
static handle(tags, implications) {
const lookup = Object.fromEntries(tags.split(' ').filter(tag => tag.length > 0).map(tag => [tag, true]));
let rules = implications.split('\n').filter(row => row.length > 0)
.map(tags => tags.split(' ').filter(tag => tag.length > 0));
rules = Object.fromEntries(rules.map(tags => [tags.shift(), tags]));
for (const [implied, matches] of Object.entries(rules)) {
if (lookup[implied]) continue;
for (const tag of matches) {
if (lookup[tag]) {
lookup[implied] = true;
break;
}
}
}
return Object.keys(lookup).sort().join(' ');
}
}
// eslint-disable-next-line no-unused-vars
class Navbar {
static bar = j('#navbar');
static add(content, index) {
if (typeof content == 'string') {
content = j(content);
}
let insert = index && j(`li:nth-child(${index - 1})`, this.bar);
if (content.is('li')) {
return index ? content.insertAfter(insert) : content.appendTo(this.bar);
} else {
let li = j('<li></li>');
return content.appendTo(index ? li.insertAfter(insert) : li.appendTo(this.bar));
}
}
static remove(selector) {
const element = j(selector, this.bar);
if (element.is('li')) {
element.remove();
} else {
element.parent().remove();
}
}
}
// eslint-disable-next-line no-unused-vars
class SubNavbar {
static bar = j('#subnavbar');
static create() {
this.bar = j('<ul class="flat-list" id="subnavbar" style="margin-bottom: 1px;"></ul>').insertAfter(Navbar.bar);
}
static add(content, index) {
if (!this.bar.length) this.create();
if (typeof content == 'string') {
content = j(content);
}
let insert = index && j(`li:nth-child(${index - 1})`, this.bar);
if (content.is('li')) {
return index ? content.insertAfter(insert) : content.appendTo(this.bar);
} else {
let li = j('<li></li>');
return content.appendTo(index ? li.insertAfter(insert) : li.appendTo(this.bar));
}
}
static remove(selector) {
if (!this.bar.length) return;
const element = j(selector, this.bar);
if (element.is('li')) {
element.remove();
} else {
element.parent().remove();
}
}
}
// eslint-disable-next-line no-unused-vars
class Notice {
static element;
static init() {
if (!getUrlParameter('page')) {
this.element = j('<div id="status-notices"></div>').prependTo('#links');
}
if (j('#status-notices').length == 0) {
this.element = j('<div id="status-notices"></div>').prependTo('#content');
}
this.element = j('#status-notices');
j('br', this.element).remove();
}
static get(match) {
const notices = j('.status-notice');
for (const notice of notices) {
if (j(`:contains(${match})`, notice).length > 0) {
return notice;
}
}
}
static add(content, options = {}) {
const notice = j(`<div class="status-notice">${content}</div>`);
if (options.dismissable) {
notice.append(' ');
j('<a href=#>Dismiss</a>').click(function() {
notice.remove();
return false
}).appendTo(notice);
}
return notice.appendTo(this.element);
}
}
// eslint-disable-next-line no-unused-vars
class Resize {
static auto(image) {
const imageRatio = image.prop('naturalWidth') / image.prop('naturalHeight');
let viewportHeight = j(window).height();
let viewportWidth = j(window).width();
viewportWidth -= image.position().left + (viewportWidth * 0.01);
viewportHeight -= viewportHeight * 0.05;
const screenRatio = viewportWidth / viewportHeight;
if (imageRatio > screenRatio) {
this.wide(image);
} else {
this.tall(image);
}
}
static disable(image) {
image.width('');
image.height('');
image.removeAttr('image-mode');
image.trigger('userscript-resized');
}
static toggle(image) {
const attr = image.attr('image-mode');
switch (attr) {
case 'wide':
this.tall(image);
break;
case 'tall':
this.wide(image);
break;
default:
this.auto(image);
break;
}
}
static tall(image) {
const viewportHeight = j(window).height();
const imageWidth = image.prop('naturalWidth');
const imageHeight = image.prop('naturalHeight');
const maxHeight = viewportHeight - (viewportHeight * 0.05);
if (imageHeight > maxHeight) {
const ratio = imageWidth / imageHeight;
image.width(maxHeight * ratio);
image.height(maxHeight);
} else {
image.width(imageWidth);
image.height(imageHeight);
}
image.attr('image-mode', 'tall');
image.trigger('userscript-resized');
}
static wide(image) {
const viewportWidth = j(window).width();
const imageWidth = image.prop('naturalWidth');
const imageHeight = image.prop('naturalHeight');
const maxWidth = viewportWidth - image.position().left - (viewportWidth * 0.01);
if (imageWidth > maxWidth) {
const ratio = imageHeight / imageWidth;
image.width(maxWidth);
image.height(maxWidth * ratio);
} else {
image.width(imageWidth);
image.height(imageHeight);
}
image.attr('image-mode', 'wide');
image.trigger('userscript-resized');
}
}
// eslint-disable-next-line no-unused-vars
class Settings {
static categories = [
'blacklist',
'greylist',
'moderator'
];
static settings = {};
static register(id, options) {
this.settings[id] = Object.assign({}, {
name: 'Invalid name',
description: 'Invalid description',
type: 'checkbox',
fallback: false
}, options);
}
static get(id) {
return Cache.get('setting_' + id, this.settings[id].fallback);
}
static getMod(id) {
if (!id) {
return this.get('is_moderator');
}
return this.get('is_moderator') && this.get(id);
}
static set(id, val) {
Cache.set('setting_' + id, val);
}
static buildOptions() {
const list = j('#user-edit > form > table > tbody');
list.append('<tr><td><label class="block">Userscript options</td></tr>');
const basic = [];
const headers = {};
for (const category of this.categories) {
headers[category] = [j(`<tr><td><label class="block">${category[0].toUpperCase() + category.substring(1)} options</td></tr>`), []];
}
for (const [id, data] of Object.entries(this.settings)) {
if (data.type == 'none') continue;
const header = data.category ? headers[data.category] : basic;
header.push(`
<tr>
<th width="15%">
<label class="block">${data.name}</label>
<p>${data.description}</p>
</th><td width="85%">
${this.buildControl(id, data)}
</td>
</tr>
`);
}
list.append(basic);
for (const category of this.categories) {
list.append(headers[category]);
}
window.eval('autocomplete_setup')();
if (this.get('blacklist') != '') {
j('#tags').val(this.get('blacklist'));
}
j('#user-edit > form').submit(() => {
for (const [id, data] of Object.entries(this.settings)) {
if (data.type == 'none') continue;
this.set(id, this.getInput(id, data));
}
this.set('blacklist', j('#tags').val());
if (this.get('replace_blacklist')) {
j('#tags').val('');
}
});
}
static buildControl(id, data) {
switch (data.type) {
case 'checkbox':
return `<input type="checkbox" id="${id}" name="${id}" ${this.get(id) ? 'checked' : ''}>`;
case 'tags':
return `
<div class="awesomplete">
<div class="awesomplete">
<textarea cols="80" id="${id}" name="${id}" rows="6" autocomplete="off" aria-autocomplete="list">${this.get(id)}</textarea>
<ul hidden=""></ul>
<span class="visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
</div>
<ul hidden=""></ul>
<span class="visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
</div>
`;
case 'select':
return `
<select id="${id}" name="${id}">
${data.options.map(option => `<option value="${option.value}" ${this.get(id) == option.value ? 'selected' : ''}>${option.name}</option>`).join()}
</select>
`
case 'none':
return;
}
}
static getInput(id, data) {
switch (data.type) {
case 'checkbox':
return j('#' + id).is(':checked');
case 'tags':
case 'select':
return j('#' + id).val();
case 'none':
return;
}
}
}
// eslint-disable-next-line no-unused-vars
class Spoilers {
static init() {
j('.spoiler').replaceWith(function() {
return '<div class="spoiler spoiler-active">' + j(this).html() + '</div>';
});
j('.spoiler').each(function() {
const element = j(this);
j('<a href="#">Spoiler</a>').insertBefore(element).click(function() {
element.toggleClass('spoiler-active');
return false;
});
});
}
}
GM_addStyle(`
.spoiler {
color: #dedece !important;
background: #272727 !important;
margin-top: 1em;
padding: 2px;
border: 1px solid #bababa;
}
.spoiler-active {
display: none;
}
`);
// eslint-disable-next-line no-unused-vars
class Tags {
static init() {
const list = j('#tag-sidebar');
j('li.tag-type-artist', list).each(function() {
const name = j('a:last()', this).text().trim();
j(this).append(`<a href="index.php?page=artist&s=list&search=${name}&commit=Search">(A)</a>`);
});
}
}
// eslint-disable-next-line no-unused-vars
class TagScript {
static css = `
.tagscript {
display: none;
left: 50%;
top: 50%;
position: absolute;
z-index: 9;
background-color: #2A2A2A;
border: 1px solid #AAAA9A;
}
.tagscript-header {
padding: 10px;
cursor: move;
}
.tagscript-selected img {
border: 3px dashed red !important;
}
.tagscript textarea {
border-left: 0px;
border-right: 0px;
resize: none;
}
.tagscript button {
box-sizing: border-box;
border: 1px solid;
color: #aaaa9a;
background: #333;
width: 99px;
height: 19px;
margin: 2px;
}
.tagscript button:hover {
background: #666;
}`;
static enabled = () => Cache.get('tagscript_enabled', false);
static init() {
GM_addStyle(this.css);
SubNavbar.add('<a href="#">TagScript</a>').click(function() {
TagScript.toggle();
return false;
});
j(document.body).append(`
<div id="tagscript" class="tagscript">
<div class="tagscript-header">TagScript Console (<a href="https://gist.github.com/TankNut/1a3b21335044a9cecfcfa603b99a8855#file-tagscript-md" target="_blank">Help</a>)<span style="float:right"><a id="tagscript-close" href="#">Close</a></span></div>
<textarea id="tagscript-entry" rows="8" cols="42"></textarea>
<button id="tagscript-apply">Apply</button>
<button id="tagscript-save">Save</button>
<button id="tagscript-clear">Deselect</button>
</div>
`);
const div = j('#tagscript');
setTimeout(function() {
div.center()
}, 0)
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0
j('.tagscript-header').mousedown(function(event) {
pos3 = event.clientX;
pos4 = event.clientY;
j(document).mousemove(function(event) {
pos1 = pos3 - event.clientX;
pos2 = pos4 - event.clientY;
pos3 = event.clientX;
pos4 = event.clientY;
const offset = div.offset();
div.css('left', offset.left - j(document).scrollLeft() - pos1);
div.css('top', offset.top - j(document).scrollTop() - pos2);
div.css('position', 'fixed');
return false;
});
j(document).mouseup(function() {
j(this).off('mouseup');
j(this).off('mousemove');
});
return false;
});
j('#tagscript-close').click(function() {
TagScript.toggle();
return false;
});
j('#tagscript-entry').val(Cache.get('tagscript_text', ''));
j('#tagscript-apply').click(function() {
const text = j('#tagscript-entry').val()
const ops = TagScript.parse(text);
const promises = [];
Cache.set('tagscript_text', text);
j('.tagscript-selected').each(function() {
const element = j(this);
const postId = element.attr('id').substr(1);
promises.push(API.editPost(postId, ops))
});
Promise.all(promises).then(() => {
window.location.reload();
});
});
j('#tagscript-save').click(function() {
Cache.set('tagscript_text', j('#tagscript-entry').val());
});
j('#tagscript-clear').click(function() {
j('.tagscript-selected').removeClass('tagscript-selected');
});
j('.thumb').click(function() {
if (TagScript.enabled()) {
const element = j(this);
element.toggleClass('tagscript-selected');
return false;
}
});
if (this.enabled()) {
div.show();
}
}
static toggle() {
const val = !this.enabled();
Cache.set('tagscript_enabled', val);
if (val) {
j('#tagscript').show();
} else {
console.log('hide');
j('#tagscript').hide();
}
}
static regex = /(\S)+/g;
static parse(str) {
const ops = {
addTags: [],
removeTags: [],
implications: Settings.get('tag_implications_mode')
};
for (const match of str.matchAll(this.regex)) {
const text = match[0];
if (text[0] == '+') { ops.addTags.push(text.substring(1)); continue; }
if (text[0] == '-') { ops.removeTags.push(text.substring(1)); continue; }
if (text.includes(':')) {
const [op, ...arg] = text.split(':');
ops[op] = arg.join(':');
} else {
ops.addTags.push(text);
}
}
return ops;
}
}
// eslint-disable-next-line no-unused-vars
class Timestamp {
static getPreference(date) {
switch (Settings.get('timestamp_format')) {
case 'relative':
return date.fromNow();
case 'calendar':
return date.calendar(null, {
sameElse: 'MMMM Do, YYYY'
});
case 'hub':
return date.local().format('ddd, MMM DD YYYY, HH:mm')
}
}
static update(element, val, format, strict) {
const date = new moment.tz(val, format, strict, 'Europe/Brussels');
element.attr('title', date.local().toString());
element.text(this.getPreference(date));
}
static get(val, format, strict) {
const date = new moment.tz(val, format, strict, 'Europe/Brussels');
return {
title: date.local().toString(),
text: this.getPreference(date)
}
}
}
// eslint-disable-next-line no-unused-vars
class Userscript {
static handlers = {};
static globals = [];
static page;
static mode;
static register(page, mode, func, css) {
this.handlers[page] = this.handlers[page] || {};
this.handlers[page][mode] = {func: func, css: css};
}
static global(func) {
this.globals.push(func);
}
static init() {
this.page = getUrlParameter('page');
this.mode = getUrlParameter('s');
}
static run() {
const data = this.handlers?.[this.page]?.[this.mode];
if (data) {
data.func && data.func();
data.css && GM_addStyle(data.css);
}
for (const callback of this.globals) {
callback();
}
}
}
GM_addStyle(`
.has-mail {
border-color: #ee8887 !important;
background: none !important;
}
#site-title a:not(:first-child) {
background-image: none !important;
}
.tag-search {
width: 200px !important;
}
`);
// eslint-disable-next-line no-unused-vars
class Version {
static check(version) {
const saved = Cache.get('version', false);
Cache.set('version', version);
if (!saved) {
Notice.add('The hub userscript has been successfully installed.', {dismissable: true});
} else if (version != saved) {
Notice.add(`The hub userscript has been updated to version ${version}.`, {dismissable: true});
}
if (!Userscript.page) {
j('#static-index > div:nth-child(8) > p').append(`<br>Running hub userscript version ${version}`);
}
}
}
Settings.register('is_moderator', {
name: 'Moderator',
description: 'Required before any of the other options in this category work.',
category: 'moderator'
});
Settings.register('admin_panel_visible', {
name: 'Admin panel visibility',
description: 'Whether or not the admin panel link should be visible regardless of where you are on the site.',
category: 'moderator'
});
Settings.register('admin_show_summary', {
name: 'Moderation summary',
description: 'Whether viewing the post index should add a summary of the mod queue to the navigation bar.',
category: 'moderator'
})
Settings.register('unhide_children', {
name: 'Unhide child posts',
description: 'Redirects you away from the main page where child posts are hidden by default.'
});
Settings.register('show_footers', {
name: 'Show post footers',
description: 'Adds an element below each post that shows the rating, score, resolution and provides a direct link to the image itself.',
fallback: true
});
Settings.register('tag_implications', {
name: 'Tag implications',
description: 'When using the custom tag implication option the first tag per line will be added if any of the others are present.',
type: 'tags',
fallback: ''
});
Settings.register('tag_implications_mode', {
name: 'Default mode',
description: 'Which tag implication mode should be auto-selected when editing a post.',
type: 'select',
options: [
{name: 'Off', value: 'off'},
{name: 'Built-in', value: 'builtin'},
{name: 'Custom', value: 'custom'},
{name: 'Both', value: 'both'}
],
fallback: 'off'
});
Settings.register('timestamp_format', {
name: 'Timestamp format',
description: 'The format to use for timestamps throughout the site.',
type: 'select',
options: [
{name: 'Relative (x hours ago)', value: 'relative'},
{name: 'Calendar (Today at HH:MM)', value: 'calendar'},
{name: 'Hub forums (Mon, Jan 01 2023, HH:MM)', value: 'hub'}
],
fallback: 'relative'
});
Settings.register('blacklist', {
type: 'none',
fallback: ''
});
Settings.register('replace_blacklist', {
name: 'Replace blacklist',
description: "Replaces the blacklist's filter with the same system used for greylists.",
category: 'blacklist'
});
Settings.register('enforce_blacklist', {
name: 'Enforce blacklist',
description: 'Removes the blacklist toggle from any pages that have it.',
category: 'blacklist'
});
Settings.register('greylist', {
name: 'Tag greylist',
description: 'Any post matching all rules on a line will get marked with a colored border. Supports #hex color and inverted (! or ~) tags.',
type: 'tags',
fallback: '',
category: 'greylist'
});
Settings.register('greylist_videos', {
name: 'Identify videos',
description: 'Gives videos a blue border.',
fallback: true,
category: 'greylist'
});
Settings.register('greylist_gifs', {
name: 'Identify gifs',
description: 'Gives animated gifs a cyan border.',
fallback: true,
category: 'greylist'
});
Userscript.register('account', 'options', function() {
Settings.buildOptions();
});
Userscript.register('artist', 'list', function() {
const results = j('#content table tbody tr').length - 1;
if (results == 1) {
window.location.replace(j('#content table tbody tr:nth-child(2) td:nth-child(2) a').attr('href'));
}
});
Userscript.register('comment', 'list', function() {
j('div.response-list div.post').each(function() {
const element = j(this);
if (element.css('display') == 'none') {
element.parents('div[id^=p]').addClass('blacklisted-image');
}
});
if (Settings.get('enforce_blacklist')) {
j('#ci').remove();
} else {
j('#ci').click(function() {
j('.blacklisted-image, .unblacklisted-image').each(function() {
j(this).toggleClass('blacklisted-image').toggleClass('unblacklisted-image');
});
return false;
});
}
j('span.date').each(function() {
const element = j(this);
Timestamp.update(element, element.text(), ' YYYY-MM-DD HH:mm:ss', true);
});
const blacklist = Settings.get('replace_blacklist') ? Filter.parse(Settings.get('blacklist')) : false;
if (blacklist) {
const posts = window.eval('posts');
let blacklistCount = 0;
for (const [postId, tags] of Object.entries(posts.tags)) {
if (Filter.match(tags, blacklist)) {
j(`#p${postId}`).addClass('blacklisted-image');
blacklistCount++;
}
}
if (blacklistCount > 0) {
j('#ci').text(`(${blacklistCount} hidden)`);
}
}
}, `.blacklisted-image {
display: none;
}
`);
Userscript.register('favorites', 'view', function() {
if (getUrlParameter('random')) {
window.location = j('span.thumb > a:nth-child(1)').random().attr('href');
return;
}
SubNavbar.add('<a href="#">Download page</a>').click(function() {
j('span.thumb > a:nth-child(1)').each(function() {
const element = j(this);
const id = element.attr('id').substr(1);
API.getPost(id).then(data => {
const extension = data.file_url.split('.').pop();
GM_download(data.file_url, `${id}.${extension}`);
});
});
return false;
});
const regex = /pid=(\d+)/;
const click = j('#paginator > a').last().attr('onclick')?.toString();
const maxPid = click?.match(regex)[1];
SubNavbar.add('<a href="#">Random post</a>').click(function() {
if (!maxPid) {
window.location = j('span.thumb > a:nth-child(1)').random().attr('href');
return false;
}
const max = Math.floor(maxPid / 50);
const index = Math.floor(Math.random() * (max + 1));
const url = new URL(window.location);
url.searchParams.set('pid', index * 50);
url.searchParams.set('random', 1);
window.location = url.href;
return false;
});
Filter.applyGreylist();
j('<input type="text" style="width:40px; margin-right:5px" placeholder="page" id="pagePicker"></input>').appendTo('#paginator');
j('<input type="submit"></input>').appendTo('#paginator').click(function() {
const num = Number(j('#pagePicker').val());
const url = new URL(window.location);
url.searchParams.set('pid', Math.max(num * 50 - 50, 0));
window.location = url.href;
return false;
});
});
Userscript.register('forum', 'list', function() {
function get(id) {
return Cache.get('forum_' + id, -1);
}
function set(id, replies) {
Cache.set('forum_' + id, replies);
}
SubNavbar.add('<a href="#">Mark as read</a>').click(function() {
j('#forum table.highlightable tbody tr').each(function() {
const id = getUrlParameter('id', j('.forum-topic a', this).attr('href'));
const replies = j('td:nth-child(5)', this).text();
set(id, replies);
});
j('.forum-topic').removeClass('unread-topic');
return false;
});
j('#forum table.highlightable tbody tr').each(function() {
const link = j('.forum-topic a', this);
const topic = j('.forum-topic', this);
const locked = j('span.locked-topic', this);
if (locked.length) {
link.css('color', 'brown');
locked.remove();
}
const id = getUrlParameter('id', link.attr('href'));
const replies = j('td:nth-child(5)', this).text();
const storedReplies = get(id);
let unread = false;
if (storedReplies < replies) {
topic.addClass('unread-topic');
unread = true;
} else {
topic.removeClass('unread-topic');
}
link.click(() => set(id, replies));
const pages = Math.floor(replies / 15);
const title = j('td:nth-child(1)', this);
title.append(` <a style="color: #666" href="?page=forum&s=view&id=${id}&pid=${pages * 15}">(Last page)</a>`)
.click(() => set(id, replies));
if (unread) {
const link = j('<a style="color: #666" href="#">(Mark read)</a>').click(function() {
set(id, replies);
topic.removeClass('unread-topic');
j(this).remove();
return false
});
title.append(' ', link);
}
const updated = j('td:nth-child(3) span', this);
Timestamp.update(updated, updated.attr('title'), "ddd, MMM DD 'YY, HH:mm");
});
});
Userscript.register('forum', 'view', function() {
j('#forum div.post').each(function() {
const element = j('div.author span.date', this);
Timestamp.update(element, element.text(), 'MM/DD/YY hh:mma', true);
});
});
Userscript.global(function() {
if (Settings.getMod('admin_panel_visible')) {
SubNavbar.remove('a[href="admin/"]');
Navbar.add('<a id="admin" href="admin/">Administration Panel</a>');
} else {
j('a[href="admin/"]').replaceWith('<a id="admin" href="admin/">Administration Panel</a>');
}
})
GM_addStyle(`
#admin {
color: #33cfff;
font-weight: bold;
}
#admin:hover {
color: #80e1ff;
}
`)
Userscript.register('pool', 'list', function() {
const activePool = Cache.get('active_pool', -1);
if (activePool > -1) {
SubNavbar.add('<a href="#">Clear active pool</a>').click(function() {
Cache.delete('active_pool');
j(this).remove();
return false;
});
}
});
Userscript.register('pool', 'order', function() {
const autoButton = j('#content > form > table > tfoot > tr > td > input[type=button]:nth-child(2)');
const reverseButton = j('#content > form > table > tfoot > tr > td > input[type=button]:nth-child(3)');
autoButton[0].removeAttribute('onclick');
reverseButton[0].removeAttribute('onclick');
autoButton.click(function() {
const interval = parseFloat(prompt('What interval to use', 1));
j('.pp').each(function(index) {
j(this).val(index * interval);
});
return false;
});
reverseButton.click(function() {
const posts = j('.pp');
const values = [];
posts.each(function(index) {
values[index] = j(this).val();
});
const offset = values.length - 1;
posts.each(function(index) {
j(this).val(values[offset - index]);
});
return false;
});
const uploadButton = j('<input type="button" value="Upload Order">').click(function() {
const posts = j('.pp');
const values = [];
posts.each(function(index) {
values[index] = [parseInt(j(this).attr('name').match(/\d+/g)[0]), parseInt(j(this).val())];
});
values.sort((a, b) => a[0] - b[0]);
for (const index in values) {
j(posts[index]).val(values[index][1]);
}
return false;
});
reverseButton.after(' ', uploadButton);
});
Userscript.register('pool', 'show', function() {
const id = getUrlParameter('id');
const name = j('#pool-show h4').text().slice(6);
const posts = j('#pool-show div > span.thumb').map(function() {
return j(this).attr('id').slice(1);
}).toArray();
Cache.set('pool_' + id, {name: name, posts: posts}, moment.duration(1, 'weeks'));
SubNavbar.add('<a href="#">Download</a>').click(function() {
j('span.thumb').each(function(i) {
const element = j(this);
const postId = element.attr('id').substr(1);
API.getPost(postId).then(data => {
const extension = data.file_url.split('.').pop();
GM_download(data.file_url, `${id}-${i + 1}-${postId}.${extension}`);
});
});
return false;
});
const activePool = Cache.get('active_pool', -1);
SubNavbar.add(`<a href="#">${activePool == id ? 'Clear active pool' : 'Set active pool'}</a>`).click(function() {
const match = activePool == id;
if (match) {
Cache.delete('active_pool');
} else {
Cache.set('active_pool', id);
}
j(this).text(match ? 'Set active pool' : 'Unset active pool');
return false;
});
TagScript.init();
});
Userscript.register('post', 'add', function() {
j('#upload-form input[type=file]').change(function() {
const file = this.files[0];
const objectUrl = URL.createObjectURL(file);
const img = new Image();
img.onload = function() {
if (this.width >= 3200 || this.height >= 2400) {
const tags = j('#tags');
tags.val('absurdres ' + tags.val());
}
URL.revokeObjectURL(objectUrl);
};
img.src = objectUrl;
})
});
Userscript.register('post', 'list', function() {
if (getUrlParameter('random')) {
window.location = j('span.thumb > a:nth-child(1)').random().attr('href');
return;
}
if (Settings.getMod('admin_show_summary')) {
API.getQueue().then(count => {
const val = count == 25 ? '24+' : count.toString();
Navbar.add(`<a id="queue" href="admin/moderate/?page=post_queue">Queue: ${val}</a>`);
});
}
j('#s123459271093').remove();
j('.blacklisted-image > a').each(function() {
j(this).css('display', '');
});
if (Settings.get('enforce_blacklist')) {
j('#blacklisted-sidebar').remove();
} else {
j('#blacklisted-sidebar > h5 > a').attr('onclick', null).click(() => {
j('.blacklisted-image, .unblacklisted-image').each(function() {
j(this).toggleClass('blacklisted-image').toggleClass('unblacklisted-image');
});
return false;
})
}
const blacklist = Settings.get('replace_blacklist') ? Filter.parse(Settings.get('blacklist')) : false;
let blacklistCount = 0;
const showFooters = Settings.get('show_footers');
j('.thumb').each(function() {
const element = j(this);
const id = element.attr('id').substr(1);
// Not sure what this is
if (id === '1942169814381') {
return
}
const tags = j('img', this).attr('title');
if (showFooters) {
const rating = tags.match(/rating:(\w+)/)[1].toLowerCase();
const score = tags.match(/score:(\d+)/)[1];
const footer = j(`<a class="image-footer"></a>`);
API.getPost(id).then((data) => {
footer.addClass(rating);
if (data == false) {
footer.addClass('unapproved');
footer.text('UNAPPROVED');
return
}
footer.text(`Score: ${score}`);
footer.attr('data-alt', `${data.width} x ${data.height}`);
footer.attr('href', data.file_url);
footer.hover(function() {
const oldText = footer.text();
const newText = footer.attr('data-alt');
footer.attr('data-alt', oldText);
footer.text(newText);
});
});
j(this).append(footer);
}
if (blacklist && Filter.match(tags, blacklist)) {
element.addClass('blacklisted-image');
blacklistCount++;
}
});
if (blacklistCount > 0) {
j('#blacklisted-sidebar').show();
j('#blacklist-count').text(blacklistCount);
} else if (blacklist) {
j('#blacklisted-sidebar').remove();
}
Filter.applyGreylist();
const options = Cache.get('popular-options', {});
const enabled = () => Cache.get('popular-search', false);
j('.tag-search').append(`
<div id="popular-settings">
<ul>
<li><h5>Popularity options</h5></li>
<li><label>Pages <input id="popular-from" type="number" value="${options.from ?? 0}"> to <input id="popular-to" type="number" value="${options.to ?? 10}"></label></li>
<li><label>Search mode <select id="popular-mode" style="width:auto;">
<option ${options.mode == 0 ? 'selected' : ''}>All posts</option>
<option ${options.mode == 1 ? 'selected' : ''}>Search only</option>
</select></label></li>
<li><button id="popular-previous" class="popular-button">Previous</button><button id="popular-next" class="popular-button">Next</button></li>
</ul>
</div>
`);
SubNavbar.add('<a id="toggle-popular" href="#">Toggle Popular Search</a>').click(function() {
const val = !enabled();
Cache.set('popular-search', val);
if (val) {
j('#popular-settings').show();
} else {
j('#popular-settings').hide();
}
return false;
});
const tags = getUrlParameter('tags');
if (enabled()) {
if (tags != 'all') {
j('.tag-search input[type="text"]').val(Cache.get('last-search', ''));
}
} else {
j('#popular-settings').hide();
}
j('#popular-previous').click(() => {
const from = Number(j('#popular-from').val());
const to = Number(j('#popular-to').val());
const offset = (to - from) + 1
j('#popular-from').val(Math.max(from - offset, 0));
j('#popular-to').val(Math.max(to - offset, 0));
j('div.tag-search > form').submit();
return false;
});
j('#popular-next').click(() => {
const from = Number(j('#popular-from').val());
const to = Number(j('#popular-to').val());
const offset = (to - from) + 1
j('#popular-from').val(from + offset);
j('#popular-to').val(to + offset);
j('div.tag-search > form').submit();
return false;
});
j('div.tag-search > form').submit(() => {
if (enabled()) {
const tags = j('input[name="tags"]').first().val().trim();
const from = j('#popular-from').val();
const to = j('#popular-to').val();
const mode = j('#popular-mode').prop('selectedIndex');
Cache.set('last-search', tags);
Cache.set('popular-options', {
from: from,
to: to,
mode: mode
});
const search = mode == 0 ? '' : tags;
const promise1 = from > 0 ? API.getSearch(search, from - 1).then(result => result?.[0]?.id ?? 0) : 0;
const promise2 = to > 0 ? API.getSearch(search, to - 1).then(result => result?.[result.length - 1]?.id ?? 0) : 0;
Promise.all([promise1, promise2]).then(results => {
const search = `${tags}${results[0] > 0 ? ' id:<='+results[0] : ''}${results[1] > 0 ? ' id:>='+results[1] : ''} sort:score`;
location.href = `https://hypnohub.net/index.php?page=post&s=list&tags=${search.replaceAll(' ', '+')}`;
});
return false;
}
});
const regex = /pid=(\d+)/;
SubNavbar.add('<a href="#">Random (Search)</a>', 6).click(function() {
const click = j('#paginator > div > a').last().attr('href')?.toString();
if (!click) {
window.location = j('span.thumb > a:nth-child(1)').random().attr('href');
return false;
}
const pid = click.match(regex)[1];
if (pid) {
const min = 0;
const max = Math.floor(pid / 42);
const index = Math.floor(Math.random() * (max - min + 1) + min);
const url = new URL(window.location);
url.searchParams.set('pid', index * 42);
url.searchParams.set('random', 1);
window.location = url.href;
}
return false;
})
TagScript.init();
}, `.blacklisted-image {
display: none;
}
.unblacklisted-image img {
border: 3px solid #ff0000 !important;
}
.image-footer {
width: 100%;
height: 17px;
position: absolute;
bottom: 0px;
display: block;
background: #111;
}
.safe {
color: #3dad3d !important;
}
.questionable {
color: #adad3d !important;
}
.explicit {
color: #ad3d3d !important;
}
.unapproved {
background: #000 !important;
}
.thumb {
width: 200px;
height: 215px;
position: relative;
}
.thumb img {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
max-height: 200px;
}
#admin {
color: #33cfff;
font-weight: bold;
}
#admin:hover {
color: #80e1ff;
}
div.tag-search input[type=submit]:hover {
background: #666;
}
button.popular-search {
box-sizing: border-box;
border: 1px solid;
color: #aaaa9a;
background: #333;
width: 100%;
height: 19px;
margin-top: 3px;
}
button.popular-search:hover {
background: #666;
}
#toggle-popular {
font-weight: bold;
}
#popular-settings {
margin-top: 0.2em;
}
#popular-settings li {
margin-bottom: 2px;
}
#popular-settings select,
#popular-settings input {
padding: 0;
box-sizing: border-box;
border: #aaaa9a 1px solid;
width: 40px;
}
#popular-settings label {
width: 200px;
display: inline-block;
}
#popular-settings ul {
margin-bottom: 0px;
}
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
button.popular-button {
box-sizing: border-box;
border: 1px solid;
color: #aaaa9a;
background: #333;
width: 99px;
height: 19px;
margin-top: 3px;
margin-left: 1px;
}
button.popular-button:hover {
background: #666;
}
#queue {
color: #FF7F50;
font-weight: bold;
}
#queue:hover {
color: #FFA483;
}
`);
Userscript.register('post', 'view', function() {
const image = j('#image');
const imageOptions = j('#post-view > div.sidebar > div:nth-child(5) > ul');
// Image resizing
if (image.length) {
image.on('userscript-resized', function() {
const ratio = j(this).width() / window.eval('image').width;
for (const note of window.eval('Note.all')) {
// Firemonkey fix
if (exportFunction) {
exportFunction(() => ratio, note);
} else {
note.ratio = () => ratio;
}
for (const p in note.fullsize) {
note.elements.box.style[p] = note.fullsize[p] * ratio + 'px';
}
}
}).dblclick(() => Resize.toggle(image)).click(event => {
if (event.ctrlKey) {
Resize.disable(image)
}
});
if (image[0].complete) {
Resize.auto(image);
} else {
image.on('load', () => Resize.auto(image));
}
j('li > a', imageOptions).each(function() {
const element = j(this);
if (element.text().includes('Original image'))
element.removeAttr('onclick');
});
ClickDrag.init(image);
}
// Pool notice
const postId = getUrlParameter('id');
let poolId;
if (Cache.has('post_pool_id')) {
j('.status-notice[id^=pool]').remove();
poolId = Cache.get('post_pool_id');
const url = new URL(window.location);
url.searchParams.set('pool_id', poolId);
history.replaceState(null, '', url);
Cache.delete('post_pool_id');
} else {
poolId = getUrlParameter('pool_id');
}
if (poolId) {
const data = Cache.get('pool_' + poolId, false);
if (!data) {
Notice.add(`This is a post belonging to <a href="index.php?page=pool&s=show&id=${poolId}">pool #${poolId}</a>. Please visit the pool's page so we can discover more about it.`);
} else {
const index = data.posts.indexOf(postId);
const endIndex = data.posts.length - 1;
let previous = '';
let next = '';
if (index > 0) {
previous = `<a href="index.php?page=post&s=view&id=${data.posts[index - 1]}&pool_id=${poolId}">« Previous</a> `;
}
if (index < data.posts.length - 1) {
next = `<a href="index.php?page=post&s=view&id=${data.posts[index + 1]}&pool_id=${poolId}">Next »</a> `;
}
Notice.add(`${previous}${next}This is post #${index + 1} in the <a href="index.php?page=pool&s=show&id=${poolId}">${data.name}</a> pool.`);
document.addEventListener('keydown', function(e) {
if (e.target != document.body) return;
let newIndex = -1;
if (e.code == 'ArrowLeft' && index > 0) {
newIndex = e.ctrlKey ? 0 : index - 1;
} else if (e.code == 'ArrowRight' && index < endIndex) {
newIndex = e.ctrlKey ? endIndex : index + 1;
}
if (newIndex != -1) {
window.location.assign(`index.php?page=post&s=view&id=${data.posts[newIndex]}&pool_id=${poolId}`);
}
});
}
}
Implications.init();
// Moderator approval
if (Settings.getMod()) {
const notice = Notice.get('This post is awaiting moderator approval.');
j('<br><br>').appendTo(notice);
j('<b>Mod actions: <a href=# style="color:green">Approve</a></b>').click(function() {
promisedRequest({
url: `https://hypnohub.net/admin/moderate/index.php?page=post_queue&action=approve&ajax=1&id=${postId}`,
responseType: 'text'
}).then(() => window.location.reload());
return false;
}).appendTo(notice);
j('<b> or </b>').appendTo(notice);
j('<b><a href=# style="color:red">Deny</a></b>').click(function() {
const reason = prompt('Reason for deleting this post');
promisedRequest({
url: `https://hypnohub.net/admin/moderate/index.php?page=post_queue&action=deny&ajax=1&id=${postId}&reason=${reason}`,
responseType: 'text'
}).then(() => window.history.back());
return false;
}).appendTo(notice);
}
// Tag editor
let tagEditorActive = false;
const header = j('.image-sublinks').append(' | ');
j('<a href=#>Tag Cleaner</a>').click(function() {
if (tagEditorActive) {
const removeList = [];
j('.tag-editor:checked').each(function() {
removeList.push(j(this).attr('tag'));
});
if (removeList.length < 1) {
j('.tag-editor').remove();
tagEditorActive = false;
return false;
}
const tagEntry = j('#tags');
tagEntry.text(tagEntry.text().split(' ').filter(tag => tag.length > 0 && !removeList.includes(tag)).join(' '));
j('#edit_form input[type="submit"]').click(); // #edit_form.submit() didn't work for some reason
} else {
j('li.tag').each(function() {
j(`<input type=checkbox class=tag-editor tag=${j('a:nth-child(2)', this).text().replaceAll(' ', '_')}>`).prependTo(this);
});
tagEditorActive = true;
}
return false;
}).appendTo(header);
// Add to pool
j('<li></li>').appendTo(imageOptions).append('<a href="#">Add to pool</a>').click(function() {
const id = prompt('What pool should this post be added to?');
if (!id) {
return;
}
const form = j(`<form action="index.php?page=pool&s=import&id=${id}" method="post">
<input id="id" name="id" type="hidden" value="${id}">
<input id="posts_${postId}" name="posts[${postId}]" type="hidden" value="0">
</form>`).appendTo(document.body);
j('<input name="commit" type="submit" value="Import">').appendTo(form).click();
return false;
});
const activePool = Cache.get('active_pool', -1);
if (activePool > -1) {
j('<li></li>').appendTo(imageOptions).append('<a href="#">Quick add to pool</a>').click(function() {
const form = j(`<form action="index.php?page=pool&s=import&id=${activePool}" method="post">
<input id="id" name="id" type="hidden" value="${activePool}">
<input id="posts_${postId}" name="posts[${postId}]" type="hidden" value="0">
</form>`).appendTo(document.body);
j('<input name="commit" type="submit" value="Import">').appendTo(form).click();
return false;
});
}
// Update timestamps
j('div.comment-right-col > div:nth-child(1) > b').each(function() {
const html = j(this).html().split('<br>');
const timestamp = Timestamp.get(html[0], 'YYYY-MM-DD HH:mm:ss');
html[0] = `<span title="${timestamp.title}">\nPosted ${timestamp.text} `;
j(this).html(html.join('<br>'));
});
// My favorites
SubNavbar.add(`<a href="index.php?page=favorites&amp;s=view&amp;id=${getCookie('user_id')}">My Favorites</a>`, 4);
// Preserve pool_id
j('#edit_form').submit(() => {
const parent = j('#edit_form input[name=parent]');
if (parent.val().length == 0) {
parent.val(0);
}
if (poolId) {
Cache.set('post_pool_id', poolId);
}
});
// Post timestamp
{
const posted = j('#stats > ul > li:nth-child(2)');
const split = posted.html().split('<br>');
const timestamp = Timestamp.get(split[0], 'YYYY-MM-DD HH:mm:ss');
split[0] = ` Posted: ${timestamp.text}`
posted.html(split.join('<br>')).attr('title', timestamp.title);
}
// Blacklisted tags
{
let blacklist = Settings.get('blacklist');
if (!Settings.get('replace_blacklist')) {
blacklist = blacklist.replaceAll(/\s/g, '\n');
}
blacklist = Filter.parse(blacklist);
j('li.tag > a:nth-child(2)').each(function() {
const element = j(this);
if (Filter.match(element.text().replaceAll(' ', '_'), blacklist)) {
element.addClass('blacklisted');
}
});
}
}, `#edit_form input[type="text"] {
display: block;
}
.blacklisted {
text-decoration: line-through !important;
}
`);
Userscript.register('wiki', 'list', function() {
const list = j('#post-list > div.content > table > tbody');
if (list.children().length == 1) {
window.location.replace(j('tr > td:nth-child(2) > a', list).attr('href'));
return;
}
j('#post-list > div.content > table > tbody > tr > td:nth-child(2) > a').each(function() {
const link = j(this);
const tag = link.text();
if (!tag.includes(':')) {
link.after(` <a style="color:#666" href="index.php?page=post&s=list&tags=${link.text()}">(search)</a>`);
}
})
});
Userscript.register('wiki', 'view', function() {
j('#content a').each(function() {
const link = j(this);
const url = new URL(link.attr('href'), document.location);
if (url.searchParams.get('page') == 'wiki' && url.searchParams.get('s') == 'list') {
const tag = url.searchParams.get('search');
if (!tag.includes(':')) {
link.after(` <a style="color:#666" href="index.php?page=post&s=list&tags=${tag.replaceAll(' ', '_')}">(search)</a>`);
}
}
});
});
if (Settings.get('unhide_children')) {
const matches = [
'https://hypnohub.net/index.php?page=post&s=list&tags=all',
'https://hypnohub.net/index.php?page=post&s=list'
];
if (matches.includes(window.location.href)) {
window.location.assign('https://hypnohub.net/index.php?page=post&s=list&tags=%20');
}
j('#navbar > li:nth-child(2) > a').attr('href', 'https://hypnohub.net/index.php?page=post&s=list&tags=%20');
}
(function() {
'use strict';
Userscript.init();
Notice.init();
Cache.init();
Spoilers.init();
Tags.init();
Version.check(GM_info.script.version);
Userscript.run();
})();

TagScript

TagScript is a sort-of spiritual successor to the tag scripts functionality that was part of the hub at some point, enabling all kinds of mass-edit functionality.

Keep in mind that as with any other tools, abusing TagScript will result in a swift ban from the hub. If you're not sure about what you're doing, don't.

How-to

Tagscript works on a set of instructions, these can be key:value pairs or simply a list of tags. The currently supported instructions are:

Instruction Result Example
+ Adds a tag +femdom
- Removes a tag -femdom
implications: Runs the userscript's tag implications feature on the tag list
Supported values: builtin, custom and both
implications:builtin
rating: Sets a post's rating
Supported values: s(safe), q(questionable) and e(explicit)
rating:e
parent: Sets the parent of a post, uses a post ID parent:163223
source: Sets the source link of a post, takes any text or URL source:https://hypnohub.net/

Any instructions that aren't resolved are instead interpreted as tags to be added (e.g. text translates to +text)

After putting in your instructions, simply click on posts to mark them for editing, then hit Apply to run the script. This will perform all of the edits and reload the page afterwards.

Copy link

ghost commented Apr 4, 2022

You should add a @updateURL to the userscript for easy updating
Yours should be: @updateURL https://gist.githubusercontent.com/TankNut/1a3b21335044a9cecfcfa603b99a8855/raw/hypnohub-userscript.user.js

@TankNut
Copy link
Author

TankNut commented Apr 5, 2022

You should add a @updateURL to the userscript for easy updating Yours should be: @updateURL https://gist.githubusercontent.com/TankNut/1a3b21335044a9cecfcfa603b99a8855/raw/hypnohub-userscript.user.js

Done, thanks for the suggestion.

@GreenDude1
Copy link

this is really amazing program and I really love using it except whenever I use it all of the images for some reason becomes small

@edatorbit
Copy link

The most recent update seems to break the site when it's active - all the pages load insanely slowly and will freeze firefox entirely for minutes on end. It even lags when trying to enlarge or save an image.

@TankNut
Copy link
Author

TankNut commented Jan 8, 2023

The most recent update seems to break the site when it's active - all the pages load insanely slowly and will freeze firefox entirely for minutes on end. It even lags when trying to enlarge or save an image.

My first guess would be that it's your network or device being slow, I've quickly installed firefox together with tampermonkey and the userscript and couldn't find any immediate issues myself but my daily driver is and always has been chrome.

If you're willing to give a bit more info:

  • Does the site run fine with the userscript disabled?
  • Does firefox use significantly more resources with the userscript enabled versus disabled?
  • Are there any tampermonkey related errors in the developer console? (F12)
  • Are you running the latest version? (Version 2.2.3, you can find it on the welcome page of the hub)
  • Did this only start happening today? When was the last time the script updated for you?

@TankNut
Copy link
Author

TankNut commented Jan 8, 2023

After taking a second look at the recent fixes I've done I think I've found the bit of code that might've caused this issue, please let me know if the issue persists on the newest version (2.2.5)

Edit: Not sure what happened but 2.2.4 didn't actually include the fix, 2.2.5 does.

@edatorbit
Copy link

Does the site run fine with the userscript disabled?

Yes, it runs normally when the script is disabled.

Does firefox use significantly more resources with the userscript enabled versus disabled?

With the script disabled, Firefox uses about 1200mb-2,000mb. With the script enabled, it shoots up to using at least 8,500mb of memory, and I've seen it go as high as 12,000mb.

Are there any tampermonkey related errors in the developer console? (F12)

There's only one error, which shows up regardless of if the script is enabled or not: "This page is in Almost Standards Mode. Page layout may be impacted. For Standards Mode use “< !DOCTYPE html>”."

Are you running the latest version? (Version 2.2.3, you can find it on the welcome page of the hub)

Yes. The site was running fine until I got a notification that the script updated to 2.2.3, and then the site broke.

Did this only start happening today? When was the last time the script updated for you?

It started happening the day I reported it (18 hours ago or so) directly after the script updated to 2.2.3.

Additional Information: The update to 2.2.5 doesn't seem to have fixed the issue, and neither does restarting Firefox or my PC. There's no problems when the script is disabled.

@TankNut
Copy link
Author

TankNut commented Jan 9, 2023

The weird thing is that 2.2.3 was just a bugfix, nothing in that update should've been able to break the script to such an extreme extent and taking up 6 to 10 gigs of memory is something I'm not sure should even be possible.

Right now another thing you could try is to completely remove the userscript from tampermonkey, restart your browser and re-add it to see if a fresh install fixes it, though you would have to manually back up your black/grey/tag list if you're using those options.

Alternatively you could try to upload the storage file after encountering the issue so I can rule out some things, for what you'd have to set Tampermonkey's config mode to advanced in the dashboard's settings, open the userscript and upload the contents of the storage tab somewhere. (The cache doesn't contain anything sensitive, just the userscript's settings and cached posts/pools/forum topics)

@edatorbit
Copy link

The weird thing is that 2.2.3 was just a bugfix

There's a chance I'm misremembering the update number. I know the script notified me it updated twice in one day on the day that it stopped working; but I haven't been on hypnohub in a while so it could have been an older update that didn't fully take effect until after I restarted my web browser. I'll try your recommendations hopefully tomorrow.

taking up 6 to 10 gigs of memory is something I'm not sure should even be possible.

When I open a page, it starts to rapidly take up more and more of firefoxes memory. If you leave the page alone after it loads without doing anything, it'll gradually return to normal in terms of memory usage, but if you try to do anything after that (and I mean ANYTHING: clicking on tampermonkey to see running scripts, trying to open a new tab, trying to click on an image, etc.) it'll skyrocket the memory usage again and freeze firefox for about a minute.

@edatorbit
Copy link

Right now another thing you could try is to completely remove the userscript from tampermonkey, restart your browser and re-add it to see if a fresh install fixes it, though you would have to manually back up your black/grey/tag list if you're using those options.

I just tried that with no luck. There's still noticeable lag, which gets incrementally worse after opening just a few more hypnohub tabs. The first tab I opened went from using 900mb to using 1800mb.

Alternatively you could try to upload the storage file after encountering the issue so I can rule out some things

I'm not sure how to download that as a file, but here's the content of the storage tab (password is "hypnohub", no quotes): https://controlc.com/1303f33b

@TankNut
Copy link
Author

TankNut commented Jan 10, 2023

Yeah that looks perfectly normal, what the fuck?

I wish I had better news but I'm completely out of ideas at this point, there doesn't seem to be any rhyme or reason as to why things are acting this way and from what I can tell nobody else has had or is having this issue. At this point the best suggestion I can give is to downgrade your script to version 1.18.0, remove the @updateURL and @downloadURL lines from the top of the file (or disable update checking through the tampermonkey dashboard) and use that instead.

Edit: There's also the option of using another userscript manager like like firemonkey or greasemonkey and see if that makes a difference but I can't help you with that.

@edatorbit
Copy link

Edit: There's also the option of using another userscript manager like like firemonkey or greasemonkey

Interestingly, the script does literally nothing when installed with Greasemonkey, but seems to be working perfectly fine when installed with Firemonkey. Thanks for helping get that fixed!

@TankNut
Copy link
Author

TankNut commented Jan 10, 2023

Can't believe that ended up being the solution, god damn it tampermonkey.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment