Skip to content

Instantly share code, notes, and snippets.

@Nachtalb
Last active January 19, 2024 10:27
Show Gist options
  • Save Nachtalb/8ae5ff116bb294d018b5b1d28deae8fc to your computer and use it in GitHub Desktop.
Save Nachtalb/8ae5ff116bb294d018b5b1d28deae8fc to your computer and use it in GitHub Desktop.
One Million Count thread Assistant script for AB. It auto adds correct number with meta, fixes wrong numbers when getting sniped, shows number context list with meta and more.
// ==UserScript==
// @name One Million Count - animebytes.tv
// @namespace Violentmonkey Scripts
// @include https://animebytes.tv/forums.php*threadid=556*
// @version 2.10.2
// @author Nachtalb
// @description One Million Count thread Assistant script for AB. It auto adds correct number with meta, fixes wrong numbers when getting sniped, shows number context list with meta and more.
// @updateURL https://gist.githubusercontent.com/Nachtalb/8ae5ff116bb294d018b5b1d28deae8fc/raw/AB-one-million-count.js
// @downloadURL https://gist.githubusercontent.com/Nachtalb/8ae5ff116bb294d018b5b1d28deae8fc/raw/AB-one-million-count.js
// @supportURL https://gist.github.com/Nachtalb/8ae5ff116bb294d018b5b1d28deae8fc
// @require https://gist.githubusercontent.com/TheDistantSea/8021359/raw/0ef72e403ae51c4860cd2af9d4d18f14c1c98b01/version_compare.js
// @icon https://animebytes.tv/static/favicon.ico
// @grant GM.addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM.xmlHttpRequest
// ==/UserScript==
// =========================================
// CONFIG
// =========================================
//
// !!! READ THIS !!!
// All user options except for `checks` and `additionalContent` are loaded via the user script manager settings. Here we only set the defaults.
// `checks` and `additionalContent` can't be loaded from the settings due to limitations by the user script API.
//
// The advanages of saving the settings in your manager are that they don't get lost during an update.
//
// In violentmonkey eg. you can find the settings a tab "Values" when inspecting the script.
const userOptions = {
// Has the same effect as emptying the checks list.
enableChecks: true,
// test = function takes number as argument
// short = used in the number context list
// pass/fail_text = text when test passes / fails
// pass/fail_colour = color when text passes / fails
// show_fail = false overrides global showFails, true has complies with global showFails
checks: [
{ test: isPrime, short: 'P', pass_text: 'prime', fail_text: "isn't prime", pass_colour: 'dodgerblue', fail_colour: 'red', show_fail: true }, // prime
{ test: isNewPage, short: 'N', pass_text: 'new page', fail_text: "isn't new page", pass_colour: '#00ffdc', fail_colour: 'red', show_fail: true }, // new page
{ test: isPalindrome, short: 'A', pass_text: 'palindrome', fail_text: "isn't palindrome", pass_colour: 'orange', fail_colour: 'red', show_fail: true }, // palindrome
{ test: isNice, short: 'I', pass_text: 'nice', fail_text: "isn't nice", pass_colour: '#fff134', fail_colour: 'red', show_fail: true }, // ends with 69
{ test: isSexy, short: 'S', pass_text: 'sexy', fail_text: "isn't sexy", pass_colour: '#ff88b5', fail_colour: 'red', show_fail: true }, // palindrome, sequential increase/decrease, repeating digits, or even-length repeating patterns
{ test: isBond, short: 'B', pass_text: 'James Bond', fail_text: "isn't james bond", pass_colour: '#d4af37', fail_colour: 'red', show_fail: true }, // ends with 007
{ test: countRepeatedEndDigits, short: 'C', test_text: countRepeatedEndDigitsText, pass_colour: '#789922', fail_colour: 'red', show_fail: true }, // last x digits repeat
],
enabledChecks: ['isPrime', 'isNewPage', 'isPalindrome', 'countRepeatedEndDigits', 'isSexy', 'isBond', 'isNice'], // Which checks to enable
// list of "placeholder: function" where the placeholder can be used in the message template. Function gets number as argument
additionalContent: {},
showFails: false, // Show fails
// Arguments in the check objects starting with pass_/fail_ will be slected based on the test so {text} will be filled with pass_text in case of the test passing and with fail_text otherwise.
checkTemplate: '[color={colour}]{text}[/color]',
checkWrapper: '\n[align=center][size=1]{rawchecks}[/size][/align]', // Template around all the checks itself
checkSeparator: ' | ', // Seperator between checks in messageTemplate
// {rawchecks} is filled with the `checkTemplate` for each check
// {checks} {rawchecks} wrapped with `checkWrapper`
// {number} is filled with the new number
messageTemplate: '[align=center]{number}[/align]{checks}',
showContextNumberList: true, // Show a list of prev and next numbers. Adds {short} coloured in {pass_colour} from the checks if test passes on that number
autoFill: true, // Disable auto fill in general (overrides preserveContent)
autoFix: true, // Detect wrong numbers and auto fix them
autoFixThirdPartyPosts: true, // Auto fix posts not made with this script (might break a post)
quicksendButton: true, // Add button to quicksend next number
autoSend: false, // Auto send if possible (auto disables after 5min)
autoSendSpecialOnly: true, // Only send numbers that at least match one check
autoReload: {
enabled: true, // Auto reload page on new posts
reloadDelay: 5000, // Delay between new posts test in ms, should not be below 2000 or you might get temp ban
scrollToInput: true, // Scroll back to input field after reload. Only affects auto update on new thread page
preserveContent: true, // Preserve inputfield content during relaod. Only affects auto update on new thread page
},
numberToImage: true,
numberImageMap: {
0: "[img]https://mei.animebytes.tv/s0kl2epEule.gif[/img]",
1: "[img]https://mei.animebytes.tv/muxJcLcihUX.gif[/img]",
2: "[img]https://mei.animebytes.tv/1KmhKwA9aVJ.gif[/img]",
3: "[img]https://mei.animebytes.tv/gYNlVUFTaby.gif[/img]",
4: "[img]https://mei.animebytes.tv/e0cC8YMGNeH.gif[/img]",
5: "[img]https://mei.animebytes.tv/ceGLCwzFcgO.gif[/img]",
6: "[img]https://mei.animebytes.tv/naBfGPFULMZ.gif[/img]",
7: "[img]https://mei.animebytes.tv/WzDpJdV6Whp.gif[/img]",
8: "[img]https://mei.animebytes.tv/gyVLEHgrApj.gif[/img]",
9: "[img]https://mei.animebytes.tv/7oRqKFP9jty.gif[/img]"
},
debug: false, // Enable debug mode
autoSendTimer: "10m", // Time until turning off autosend ([m]inutes, [h]ours and [d]ays are supported)
}
getSetConfig(userOptions, ['additionalContent', 'checks'])
const systemOptions = {
preservedContentKey: 'current',
autoReloadKey: 'autoreload',
seperator: '​',
inputField: $('#quickpost'),
onLastPage: $('.page-link').last().hasClass('nolink'),
firstUpdateDone: false,
icons: {
reload: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23789922' viewBox='0 0 489.711 489.711'%3E%3Cpath d='M112.156 97.111c72.3-65.4 180.5-66.4 253.8-6.7l-58.1 2.2c-7.5.3-13.3 6.5-13 14 .3 7.3 6.3 13 13.5 13h.5l89.2-3.3c7.3-.3 13-6.2 13-13.5v-1.6l-3.3-88.2c-.3-7.5-6.6-13.3-14-13-7.5.3-13.3 6.5-13 14l2.1 55.3c-36.3-29.7-81-46.9-128.8-49.3-59.2-3-116.1 17.3-160 57.1-60.4 54.7-86 137.9-66.8 217.1 1.5 6.2 7 10.3 13.1 10.3 1.1 0 2.1-.1 3.2-.4 7.2-1.8 11.7-9.1 9.9-16.3-16.8-69.6 5.6-142.7 58.7-190.7zm350.3 98.4c-1.8-7.2-9.1-11.7-16.3-9.9-7.2 1.8-11.7 9.1-9.9 16.3 16.9 69.6-5.6 142.7-58.7 190.7-37.3 33.7-84.1 50.3-130.7 50.3-44.5 0-88.9-15.1-124.7-44.9l58.8-5.3c7.4-.7 12.9-7.2 12.2-14.7s-7.2-12.9-14.7-12.2l-88.9 8c-7.4.7-12.9 7.2-12.2 14.7l8 88.9c.6 7 6.5 12.3 13.4 12.3.4 0 .8 0 1.2-.1 7.4-.7 12.9-7.2 12.2-14.7l-4.8-54.1c36.3 29.4 80.8 46.5 128.3 48.9 3.8.2 7.6.3 11.3.3 55.1 0 107.5-20.2 148.7-57.4 60.4-54.7 86-137.8 66.8-217.1z'/%3E%3C/svg%3E",
fix_posts: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke='%23ffa600' fill='none' stroke-linejoin='round' viewBox='0 0 24 24'%3E%3Cpath d='M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z'/%3E%3C/svg%3E",
send: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%2394d82d' viewBox='0 0 448 512'%3E%3Cpath d='M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9 190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284 16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z'/%3E%3C/svg%3E",
autosend_off: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23adb5bd' viewBox='0 0 640 512'%3E%3Cpath d='M32 224h32v192H32a31.962 31.962 0 01-32-32V256a31.962 31.962 0 0132-32zm512-48v272a64.063 64.063 0 01-64 64H160a64.063 64.063 0 01-64-64V176a79.974 79.974 0 0180-80h112V32a32 32 0 0164 0v64h112a79.974 79.974 0 0180 80zm-280 80a40 40 0 10-40 40 39.997 39.997 0 0040-40zm-8 128h-64v32h64zm96 0h-64v32h64zm104-128a40 40 0 10-40 40 39.997 39.997 0 0040-40zm-8 128h-64v32h64zm192-128v128a31.962 31.962 0 01-32 32h-32V224h32a31.962 31.962 0 0132 32z'/%3E%3C/svg%3E",
autosend_special: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23DE6321' viewBox='0 0 640 512'%3E%3Cpath d='M32 224h32v192H32a31.962 31.962 0 01-32-32V256a31.962 31.962 0 0132-32zm512-48v272a64.063 64.063 0 01-64 64H160a64.063 64.063 0 01-64-64V176a79.974 79.974 0 0180-80h112V32a32 32 0 0164 0v64h112a79.974 79.974 0 0180 80zm-280 80a40 40 0 10-40 40 39.997 39.997 0 0040-40zm-8 128h-64v32h64zm96 0h-64v32h64zm104-128a40 40 0 10-40 40 39.997 39.997 0 0040-40zm-8 128h-64v32h64zm192-128v128a31.962 31.962 0 01-32 32h-32V224h32a31.962 31.962 0 0132 32z'/%3E%3C/svg%3E",
autosend_on: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%232C4DF0' viewBox='0 0 640 512'%3E%3Cpath d='M32 224h32v192H32a31.962 31.962 0 01-32-32V256a31.962 31.962 0 0132-32zm512-48v272a64.063 64.063 0 01-64 64H160a64.063 64.063 0 01-64-64V176a79.974 79.974 0 0180-80h112V32a32 32 0 0164 0v64h112a79.974 79.974 0 0180 80zm-280 80a40 40 0 10-40 40 39.997 39.997 0 0040-40zm-8 128h-64v32h64zm96 0h-64v32h64zm104-128a40 40 0 10-40 40 39.997 39.997 0 0040-40zm-8 128h-64v32h64zm192-128v128a31.962 31.962 0 01-32 32h-32V224h32a31.962 31.962 0 0132 32z'/%3E%3C/svg%3E",
debug: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c72d41' viewBox='0 0 511.979 511.979' %3E%3Cpath d='M330 255c0-24.813-20.187-45-45-45h-15v-35.729l24.876-12.438c1.105-.553 2.393-.495 3.444.155S300 163.764 300 165c0 8.284 6.716 15 15 15s15-6.716 15-15c0-11.709-5.947-22.375-15.907-28.531-9.961-6.157-22.162-6.706-32.634-1.469l-19.751 9.875-15.184-30.368C260.5 106.863 270 92.021 270 75c0-8.284-6.716-15-15-15s-15 6.716-15 15c0 8.271-6.729 15-15 15h-60c-8.271 0-15-6.729-15-15 0-8.284-6.716-15-15-15s-15 6.716-15 15c0 17.021 9.5 31.863 23.476 39.508l-15.184 30.368-19.75-9.875c-10.474-5.237-22.673-4.688-32.634 1.468C65.947 142.625 60 153.291 60 165c0 8.284 6.716 15 15 15s15-6.716 15-15c0-1.236.628-2.362 1.68-3.012a3.518 3.518 0 013.445-.155l24.975 12.438V210H105c-24.813 0-45 20.187-45 45 0 8.284 6.716 15 15 15s15-6.716 15-15c0-8.271 6.729-15 15-15h16.509c2.384 11.695 7.512 22.397 14.659 31.434C125.924 282.1 120.1 296.425 120.1 311.83V315c0 8.284 6.616 15 14.9 15s15-6.716 15-15v-3.171c0-8.136 3.397-15.654 9.193-20.945C169.84 296.694 182.041 300 195 300s25.16-3.306 35.807-9.116c5.796 5.291 9.293 12.809 9.293 20.945V315c0 8.284 6.616 15 14.9 15s15-6.716 15-15v-3.171c0-15.405-5.924-29.73-16.168-40.396 7.147-9.036 12.275-19.738 14.659-31.433H285c8.271 0 15 6.729 15 15 0 8.284 6.716 15 15 15s15-6.716 15-15zM215.729 120l15 30H159.27l15-30h41.459zM150 225v-45h30.1v87.42C162.641 261.228 150 244.555 150 225zm60-45h30.1v45c0 19.555-12.641 36.228-30.1 42.42V180z'/%3E%3Cpath d='M195 0C87.477 0 0 87.477 0 195s87.477 195 195 195c37.494 0 72.544-10.647 102.304-29.062L435.187 498.82c17.544 17.544 46.09 17.544 63.635 0 17.543-17.544 17.543-46.091 0-63.635L360.938 297.304C379.353 267.544 390 232.494 390 195 390 87.477 302.523 0 195 0zM30 195c0-90.981 74.019-165 165-165s165 74.019 165 165-74.019 165-165 165S30 285.982 30 195zm447.608 282.608c-5.848 5.848-15.362 5.846-21.209 0l-81.397-81.397 21.209-21.209 81.397 81.397c5.848 5.847 5.847 15.361 0 21.209zm-102.61-123.819l-21.209 21.209-31.983-31.983a196.809 196.809 0 0021.209-21.209l31.983 31.983z'/%3E%3C/svg%3E",
}
}
const config = {...systemOptions, ...userOptions}
const migrations = {
'2.6.0': [
fixMessageTemplate_2_6_0,
],
}
runMigrations()
let css = `
.number-list {
padding: 0;
margin-right: 1em;
vertical-align: top;
display: inline-block;
text-align: left;
}
.number-list p {
padding: 0.5em;
}
.number-list .currentNumber {
color: #0090ff;
font-weight: bold;
font-size: 1.2em;
}
.timer {
position: fixed;
top: 4em;
right: 1em;
display: flex;
flex-direction: row;
z-index: 100;
}
.timer.enabled {
color: green;
}
.action-icons {
position: fixed;
top: 1em;
right: 1em;
display: flex;
flex-direction: row;
z-index: 100;
}
.icon {
width: 40px;
height: 40px;
display: inline-block;
margin-left: 1em;
background-repeat: no-repeat;
cursor: pointer;
display: none;
}
.icon-reload { animation: rotation 2s infinite linear; }
.icon-debug { display: ${config.debug ? 'inline-block' : 'none'}; }
.icon-fix_posts[data-tofix]:not([data-tofix=""]):not([data-tofix="0"]) {
display: inline-block;
}
.icon-fix_posts[data-tofix]:not([data-tofix=""]):not([data-tofix="0"]):after {
content: attr(data-tofix);
position: absolute;
bottom: 0;
right: 0;
background: #ffa600;
display: flex;
box-sizing: border-box;
border-radius: 50%;
color: white;
border: 1px solid white;
justify-content: center;
align-content: center;
width: 20px;
height: 20px;
font-size: 14px;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
`
/**
* Adds action icons to the page.
*/
function _addIcons () {
let iconTemplate = '<span class="icon icon-{name}" title="{title}"></span>';
let html = '<div class="action-icons">';
for (const key in config.icons) {
html += iconTemplate.replaceAll('{name}', key).replaceAll('{title}', key.replace('_', ' '));
css += `.icon-${key} { background-image: url("${config.icons[key]}"); }\n`;
}
html += '</div>'
$('body').append(html)
}
/**
* Adds a timer display to the page.
*/
function _addTimer() {
const html = '<span class="timer disabled">00:00:00</span>';
$("body").append(html)
}
/**
* Adds HTML elements for icons and timer to the page.
*/
function addHTML() {
_addIcons();
_addTimer();
}
addHTML();
$("#preview_button").hide();
GM.addStyle(css);
// =========================================
// MIGRATIONS
// =========================================
function fixMessageTemplate_2_6_0() {
config.messageTemplate = config.messageTemplate.replace('\n[align=center][size=1]{checks}[/size][/align]', '{checks}');
GM_setValue('messageTemplate', config.messageTemplate);
}
// =========================================
// UTILS
// =========================================
/**
* Converts a human-readable time string into seconds.
* The input string can contain multiple time values, each followed by a suffix:
* 'm' for minutes, 'h' for hours, and 'd' for days.
* For example: "2h15m" will be converted to 8100 seconds.
*
* @param {string} str - The human-readable time string.
* @return {number} The total time in seconds.
*/
function convertToSeconds(str) {
return (str.match(/\d+[mhd]/g) || []).reduce((acc, val) => {
const num = parseInt(val);
return acc + num * (val.endsWith('m') ? 60 : val.endsWith('h') ? 3600 : 86400);
}, 0);
}
/**
* Converts seconds into a human-readable time format.
* The output format includes days, hours, and minutes.
* Seconds and values smaller than a minute are discarded.
*
* @param {number} seconds - The time in seconds.
* @return {string} The time in a human-readable format.
*/
function secondsToReadableTime(seconds) {
const days = Math.floor(seconds / 86400);
seconds %= 86400;
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
let result = '';
if (days > 0) result += `${days}d `;
if (hours > 0 || days > 0) result += `${hours}h `;
if (minutes > 0) result += `${minutes}m`;
return result.trim();
}
/**
* Automatically adjusts the height of a textarea based on its content.
*/
function autoSizeTextArea() {
print('autosize text area');
config.inputField.each(function () {
this.setAttribute("style", "height:" + (this.scrollHeight) + "px;overflow-y:hidden;");
}).on("input", function () {
this.style.height = 0;
this.style.height = (this.scrollHeight) + "px";
});
}
/**
* Throttle function: Ensures that a function is called at most once every specified number of milliseconds
* Usage:
* var throttledFn = throttle(yourFunction, 200);
* element.addEventListener('event', throttledFn);
* This ensures 'yourFunction' is called at most once every 200ms, regardless of how often the event is triggered.
*
* @param {Function} fn - The function to be throttled
* @param {number} threshhold - The minimum time interval (in milliseconds) between successive calls
* @param {object} [scope] - An optional scope in which to execute the function (the value of 'this')
*/
function throttle(fn, threshhold, scope) {
threshhold = threshhold || 250;
var last, deferTimer;
return function() {
var context = scope || this;
var now = +new Date(),
args = arguments;
if (last && now < last + threshhold) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function() {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
};
}
/**
* Generates a preview of the message in the forum's BBCode format.
*/
function preview() {
$.post($bbcodePreview, $("#quickpostform").serialize(), function(e) {
document.getElementById("quickreplypreview").innerHTML = e;
document.getElementById("quickreplypreview").style.display = "block";
})
}
/**
* Retrieves and sets configuration options, handling default values and storage.
*
* @param {object} options - The configuration options.
* @param {string[]} ignroeKeys - Keys to ignore during configuration.
* @param {string} [prefix] - Prefix for configuration keys.
*/
function getSetConfig(options, ignroeKeys, prefix) {
const available = GM_listValues();
for (const key in options) {
if (ignroeKeys.includes(key)) continue;
const settingsKey = (prefix ? prefix + '.' : '') + key;
if (typeof options[key] === 'object' && !Array.isArray(options[key])) {
getSetConfig(options[key], [], settingsKey)
continue;
} else if (Array.isArray(options[key])) {
if (available.includes(settingsKey)) options[key] = GM_getValue(settingsKey).split(',').filter(i => i);
else GM_setValue(settingsKey, options[key].join(','))
} else {
if (available.includes(settingsKey)) options[key] = GM_getValue(settingsKey)
else GM_setValue(settingsKey, options[key])
}
}
}
/**
* Runs migrations for the script based on version changes.
*/
function runMigrations() {
const previousVersion = GM_getValue('currentVersion');
const currentVersion = GM_info.script.version;
if (currentVersion !== previousVersion) {
for (const key in migrations) {
if (previousVersion === undefined || versionCompare(previousVersion, currentVersion) < 0) {
for (const migration of migrations[key]) {
migration();
}
}
}
GM_setValue('currentVersion', currentVersion)
}
}
/**
* Prints logs to the console with the script's context.
*
* @param {...string} text - The text to be printed.
*/
function print(...text) {
let first = text.shift(); // enable print('text %c colored text', 'color: red;')
console.log('One Million count: ' + first, ...text);
}
/**
* Prints the enabled status of a feature to the console.
*
* @param {string} text - Description of the feature.
* @param {boolean} on - Whether the feature is on or off.
*/
function printEnabled (text, on) {
console.log(`${text} [%c${on ? 'ON': 'OFF'}%c]`, `color: ${on ? 'lightgreen': 'red'};`, 'color: white;');
}
/**
* Prints the current configuration to the console.
*/
function printConfig() {
(config.debug ? console.group : console.groupCollapsed)('One Million count: %cCONFIG', 'color: orange;')
console.log('-----------------------------------')
printEnabled('debug', config.debug);
printEnabled('auto fix', config.autoFix);
printEnabled('auto fix not made with this script', config.autoFixThirdPartyPosts);
printEnabled('auto fill', config.autoFill);
printEnabled('auto reload', config.autoReload.enabled);
printEnabled('auto reload - preserve input', config.autoReload.preserveContent);
printEnabled('auto reload - scroll to input', config.autoReload.scrollToInput);
printEnabled('auto send', config.autoSend);
printEnabled('auto send - special only', config.autoSendSpecialOnly);
printEnabled('quick send button', config.quicksendButton);
printEnabled('checks', config.checks.length > 0 && config.enabledChecks.length > 0 && config.enableChecks);
console.log('enabled checks [', config.enabledChecks.join(', '), ']');
printEnabled('context number list', config.showContextNumberList);
console.log('-----------------------------------')
console.groupEnd()
}
/**
* Adds a debugger function.
*/
function addDebugger() {
$(document).on('click', '.icon-debug', function (event) {
debugger;
});
}
/**
* Converts a number to a string representation using a mapping of images.
*
* @param {number} num - The number to be converted.
* @return {string} The string representation of the number.
*/
function numberToString(num) {
return String(num).split('').map(digit => config.numberImageMap[digit]).join('');
}
/**
* Generates formatted text for a message based on a given number.
*
* @param {number} number - The number to create a message for.
* @return {string} The formatted message text.
*/
function messageText(number) {
let content = config.messageTemplate;
let checks = [];
for (const {passed, check} of runChecks(number)) {
let checkText = config.checkTemplate;
for (const key in check) {
checkText = checkText.replace(`{${key}}`, check[key]);
if ((key.startsWith('pass_') && passed) || (key.startsWith('fail_') && !passed)) checkText = checkText.replace(`{${key.slice(5)}}`, check[key]);
}
checks.push(checkText);
}
content = content.replaceAll('{checks}', checks.length > 0 ? config.checkWrapper : '');
content = content.replaceAll('{rawchecks}', checks.join(config.checkSeparator))
for (const key in config.additionalContent) {
let extra = config.additionalContent[key](number);
content = content.replaceAll(key, extra !== null ? extra : '');
}
if (config.numberToImage) {
content = content.replaceAll('{number}', numberToString(number) + `\n[color=#292929]${number}[/color]`);
} else {
content = content.replaceAll('{number}', number)
}
return config.seperator + content + config.seperator;
}
/**
* Retrieves the next number for posting.
*
* @return {number} The next number.
*/
function nextNumber() {
return getNumberByPost() + 1;
}
/**
* Retrieves the number associated with a specific post.
*
* @param {number} [postId] - The ID of the post.
* @return {number} The number associated with the post.
*/
function getNumberByPost(postId) {
let index;
if (postId) {
index = Array.from(document.querySelectorAll('.post_block')).findIndex(el => el.id.slice(4) === postId.toString()) + 1;
} else {
index = document.querySelectorAll('.post_block').length;
}
return parseInt($('.page-link.nolink').first().prev().text()) * 25 + index;
}
/**
* Generates a new URL for the thread page.
*
* @return {string} The new URL.
*/
function getNewURL() {
const randInt = Math.floor(Math.random() * (2000000 - 1000000 + 1)) + 1000000;
return 'https://animebytes.tv/forums.php?action=viewthread&threadid=556&page=' + randInt;
}
/**
* Replaces the number in a message with a new number.
*
* @param {string} text - The original text.
* @param {number} newNumber - The new number to replace with.
* @return {string} The text with the number replaced.
*/
function replaceNumberMessage(text, newNumber) {
const newText = messageText(newNumber);
const start = text.indexOf(config.seperator);
const end = text.lastIndexOf(config.seperator);
if (start !== end) {
text = text.slice(null, start) + newText + text.slice(end + 1);
}
return text;
}
/**
* Determines if it's the user's turn to post.
*
* @return {boolean} True if it's the user's turn, otherwise false.
*/
function isOurTurn() {
return $('.post_block').last().find('.com-edit').length !== 1
}
/**
* Submits the post form.
*/
function submitForm() {
$('#quickpostform').submit();
}
/**
* Runs checks on a given number and returns the results.
*
* @param {number} number - The number to check.
* @return {array} An array of check results.
*/
function runChecks(number){
if (!config.enableChecks) return [];
const passed = [];
for (const check of config.checks.filter(check => config.enabledChecks.includes(check.test.name))) {
let pass = check.test(number);
if (check.test_text) {
const res = check.test_text(pass);
pass = res.pass;
if (pass) {
check.pass_text = res.text;
} else {
check.fail_text = res.text;
}
}
if ((config.showFails && check.show_fail) || pass ) {
passed.push({passed: pass, check: check})
}
}
return passed;
}
/**
* Retrieves the checks that have passed for a given number.
*
* @param {number} number - The number to check.
* @return {array} An array of passed checks.
*/
function passedChecks(number){
return runChecks(number).filter((item) => item.passed).map(item => item.check);
}
/**
* Formats a time in seconds to a string in the format HH:MM:SS.
*
* @param {number} seconds - The number of seconds.
* @return {string} The formatted time string.
*/
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secondsRemaining = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secondsRemaining.toString().padStart(2, '0')}`;
}
// =========================================
// AUTO FIX
// =========================================
/**
* Automatically fixes posts with incorrect numbering.
*/
function autoFixWrongNumbered() {
if (!config.autoFix) return;
print('Checking for wrong numbers')
const toFix = [];
$('.com-edit').closest('.post_block').each(function () {
const postId = this.id.slice(4);
const actualNumber = getNumberByPost(postId);
const currentNumber = (ids = this.querySelector('.post').textContent.match(/\d{6,7}/g)).length > 0 ? ids[0] : null;
if (currentNumber === actualNumber.toString()) return;
print(`Fixing post #${postId} which has "${currentNumber}" instead of "${actualNumber}"`)
toFix.push({postId: postId, actualNumber: actualNumber});
});
if (toFix.length === 0) return;
const icon = $('.icon-fix_posts');
icon.attr('data-tofix', toFix.length);
for (const {postId, actualNumber} of toFix) {
GM.xmlHttpRequest({
method: 'GET',
url: window.location.pathname + '?action=get_post&post=' + postId,
onload: (response) => {
let newText = replaceNumberMessage(response.responseText, actualNumber);
if (newText == response.responseText && config.autoFixThirdPartyPosts) {
print(`%cThe post #${postId} was not made with this script, the update might break the post.`, 'color: orange;');
newText = newText.replace(/\d{6,}/g, actualNumber);
} else if (newText == response.responseText) {
print(`%cThe post #${postId} was not made with this script, and thus not updated (enable autoFixThirdPartyPosts to update posts like this).`, 'color: red;');
return;
}
GM.xmlHttpRequest({
method: "POST",
url: window.location.pathname + '?action=takeedit',
data: `auth=${$currentUser.authKey}&post=${postId}&body=${encodeURIComponent(newText)}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
onload: function(response) {
print(`Post #${postId} fixed`)
document.getElementById("content" + postId).innerHTML = response.responseText;
icon.attr('data-tofix', parseInt(icon.attr('data-tofix')) - 1);
}
});
}
});
}
}
// =========================================
// QUCKSEND
// =========================================
/**
* Checks whether a quick post can be sent.
*/
function canQuicksend() {
if (!config.quicksendButton) return;
print('check for if we can quickpost')
if (isOurTurn()) $('.icon-send').show()
else $('.icon-send').hide();
}
$(document).on('newPosts', canQuicksend).on('click', '.icon-send', submitForm);
// =========================================
// AUTOSEND
// =========================================
/**
* Automatically sends the next number in the sequence.
*/
function autosend() {
if (!config.autoSend || !isOurTurn() || !config.firstUpdateDone) return;
if (config.autoSendSpecialOnly && passedChecks(nextNumber()).length === 0) return;
print('Auto send next number');
submitForm();
}
/**
* Sets the icon for the auto-send feature based on the current configuration.
*/
function setAutosendIcon() {
if (config.autoSend && config.autoSendSpecialOnly) {
$('.icon-autosend_on').hide();
$('.icon-autosend_special').show();
$('.icon-autosend_off').hide();
} else if (config.autoSend) {
$('.icon-autosend_special').hide();
$('.icon-autosend_on').show();
$('.icon-autosend_off').hide();
} else {
$('.icon-autosend_special').hide();
$('.icon-autosend_on').hide();
$('.icon-autosend_off').show();
}
}
/**
* Changes the configuration for the auto-send feature.
*
* @param {boolean} enabled - Whether auto-send is enabled.
* @param {boolean} specialOnly - Whether to send only special numbers.
*/
function changeAutosendConfig(enabled, specialOnly) {
config.autoSend = enabled;
config.autoSendSpecialOnly = specialOnly;
console.group('One Million count: %cChange config', 'color: orange;')
printEnabled('auto send', config.autoSend);
printEnabled('auto send - special only', config.autoSendSpecialOnly);
console.groupEnd();
GM_setValue('autoSend', config.autoSend);
GM_setValue('autoSendSpecialOnly', config.autoSendSpecialOnly);
if (config.autoSend) GM_setValue('autoSendLastEnabled', Date.now());
setAutosendIcon();
}
/**
* Toggles the auto-send feature.
*/
function toggleAutosend() {
if (config.autoSend && config.autoSendSpecialOnly) changeAutosendConfig(true, false)
else if (config.autoSend) changeAutosendConfig(false, false)
else changeAutosendConfig(true, true);
autosend();
}
/**
* Calculates the time since the last auto-send activation.
*
* @return {number} The time in milliseconds since the last activation.
*/
function timeSinceLastAutosendActivation() {
const lastEnabled = GM_getValue('autoSendLastEnabled');
if (lastEnabled === undefined) return -1
return Date.now() - lastEnabled
}
$(document).on('click', '.icon-autosend_on, .icon-autosend_off, .icon-autosend_special', toggleAutosend);
$(document).on('postsUpdated', autosend);
/**
* Updates the remaining time display for the auto-send feature.
*/
function updateRemainingTime() {
const timerElement = document.querySelector('.timer');
timerElement.setAttribute('title', `Current Setting: ${config.autoSendTimer}`);
if (!config.autoSend || timeSinceLastAutosendActivation() === -1) {
const fullTimeInSeconds = convertToSeconds(config.autoSendTimer);
const fullTimeFormatted = formatTime(fullTimeInSeconds);
timerElement.textContent = fullTimeFormatted;
timerElement.classList.add('disabled');
timerElement.classList.remove('enabled');
return;
}
timerElement.classList.remove('disabled');
timerElement.classList.add('enabled');
const timeSinceLastActivation = timeSinceLastAutosendActivation();
const allowedActiveTime = convertToSeconds(config.autoSendTimer) * 1000;
const remainingTime = allowedActiveTime - timeSinceLastActivation;
if (remainingTime <= 0) {
print('autosend timer reached 0');
changeAutosendConfig(false, false);
timerElement.classList.add('disabled');
timerElement.textContent = formatTime(allowedActiveTime / 1000);
} else {
timerElement.textContent = formatTime(Math.floor(remainingTime / 1000));
}
}
/**
* Adjusts the auto-send timer by a specified number of minutes.
*
* @param {number} changeInMinutes - The number of minutes to adjust the timer by.
*/
function adjustAutoSendTimer(changeInMinutes) {
let currentTimerSeconds = convertToSeconds(config.autoSendTimer);
let newTimerSeconds = Math.max(currentTimerSeconds + changeInMinutes * 60, 0);
config.autoSendTimer = secondsToReadableTime(newTimerSeconds);
GM_setValue('autoSendTimer', config.autoSendTimer);
print(`autoSendTimer changed to: ${config.autoSendTimer}`);
changeAutosendConfig(false, false);
updateRemainingTime();
}
document.addEventListener('wheel', function(event) {
if (event.target.matches('.icon-autosend_on, .icon-autosend_special, .icon-autosend_off')) {
event.preventDefault();
let changeInMinutes = event.deltaY < 0 ? 1 : -1;
adjustAutoSendTimer(changeInMinutes);
}
}, { passive: false });
// =========================================
// AUTO RELOAD
// =========================================
/**
* Shows the reload icon.
*/
function showReloadIcon() {
$('.icon-reload').show();
}
/**
* Hides the reload icon.
*/
function hideReloadIcon() {
$('.icon-reload').hide();
}
/**
* Automatically fills in the post form with the next number.
*/
function autoFill() {
if (!config.autoFill) return;
print('fill input');
const isAutoReload = localStorage.getItem(config.autoReloadKey);
let prevInput = config.autoReload.preserveContent && isAutoReload? localStorage.getItem(config.preservedContentKey) || '' : '';
if (prevInput) print('previous input preserved');
localStorage.removeItem(config.preservedContentKey);
localStorage.removeItem(config.autoReloadKey);
const newNumber = nextNumber();
let newContent = prevInput ? replaceNumberMessage(prevInput, newNumber) : messageText(newNumber);
config.inputField.val(newContent);
config.inputField.trigger("input");
if (config.autoReload.scrollToInput && isAutoReload) config.inputField.parents('.box').prev()[0].scrollIntoView();
$(document).trigger('formFilled');
throttledUpdatePreview();
}
$(document).on('newPosts', autoFill);
/**
* Reloads the page and preserves the content in the input field.
*/
function reloadPage() {
print('reload page');
localStorage.setItem(config.preservedContentKey, config.inputField.val())
localStorage.setItem(config.autoReloadKey, true)
window.open(getNewURL(), '_self');
}
/**
* Checks for new posts and updates the page accordingly.
*/
async function checkForUpdates() {
if (!config.autoReload.enabled) return;
print('check for updates');
showReloadIcon();
GM.xmlHttpRequest({
url: getNewURL(),
onload: (response) => {
const dom = new DOMParser().parseFromString(response.responseText, 'text/html');
const posts = $('.post_block', dom);
const cids = $('.post_block').get().map(e => e.id);
if (cids.length > posts.length) {
reloadPage();
return;
}
const lastChild = $('#' + cids[cids.length - 1]);
let newPosts = false;
for (const el of posts.get().reverse()) {
const currentPost = $('#' + el.id);
if (currentPost.length === 0) {
print('Add new post: %c' + el.id.slice(4), 'color: green;')
lastChild.after(el);
newPosts = true;
} else {
// if (currentPost.find('form').length === 0) // Only Update own ones
currentPost.html(el.innerHTML);
}
}
hideReloadIcon();
config.firstUpdateDone = true;
if (newPosts) {
localStorage.setItem(config.preservedContentKey, config.inputField.val())
localStorage.setItem(config.autoReloadKey, true)
$.when($(document).trigger('newPosts')).done(() => {
$(document).trigger('postsUpdated');
})
} else {
$(document).trigger('postsUpdated');
}
setTimeout(checkForUpdates, config.autoReload.reloadDelay);
}
})
}
// =========================================
// NUMBER CONTEXT LIST
// =========================================
/**
* Add a number to ther post number preview list.
*/
function addNumToList(num, box, newNumber) {
let html = "";
html += num;
for (const check of passedChecks(num)) {
html += ` <span style="color: ${check.pass_colour};" title="${check.pass_text}">${check.short}</span>`;
}
if (num === newNumber) { // Is current number
html = '<span class="currentNumber">' + html + '</span>'
}
box.append('<p>' + html + '</p>');
}
/**
* Adds a list of context numbers to the input area.
*/
function addContextNumbers() {
if (!config.showContextNumberList) return;
print('add context numbers list');
const newNumber = nextNumber();
const container = config.inputField.parents('#quickreplytext');
let box = container.find('.number-list');
if (box.length === 0) {
box = container.prepend('<p class="number-list"></p>').find('.number-list');
}
box.html('');
for (const i of [...Array(4).keys()].map(k => k + newNumber - 1)) {
addNumToList(i, box, newNumber)
}
}
$(document).on('newPosts', addContextNumbers);
// =========================================
// CHECKS
// =========================================
/**
* Determines if a given number is prime.
*
* @param {number} num - The number to check.
* @return {boolean} True if the number is prime, otherwise false.
*/
function isPrime (num) {
for(var i = 2; i < num; i++)
if(num % i === 0) return false;
return num > 1;
}
/**
* Determines if a given number is a palindrome.
*
* @param {number} num - The number to check.
* @return {boolean} True if the number is a palindrome, otherwise false.
*/
function isPalindrome (num) {
let factor = 1;
while (num / factor >= 10){
factor *= 10;
}
while (num) {
let first = Math.floor(num / factor);
let last = num % 10;
if (first != last){
return false;
}
num = Math.floor((num % factor) / 10);
factor = factor / 100;
}
return true;
};
/**
* Counts the number of repeated digits at the end of a given number and returns the corresponding text.
*
* @param {number} num - The number to check.
* @return {object} An object with pass/fail status and corresponding text.
*/
function countRepeatedEndDigitsText(num) {
if (num < 2) {
return { pass: false, text: "isn't checked" };
} else {
const endDigitLingo = ['checked', 'trips', 'quads', 'quints', 'sexts', 'septs', 'octs', 'nonts'];
return { pass: true, text: endDigitLingo[num - 2] };
}
}
/**
* Counts the number of repeated digits at the end of a number.
*
* @param {number} num - The number to check.
* @return {number} The count of repeated end digits.
*/
function countRepeatedEndDigits(num) {
const strNum = num.toString();
let count = 1;
for (let i = strNum.length - 2; i >= 0; i--) {
if (strNum[i] === strNum[strNum.length - 1]) {
count++;
} else {
break;
}
}
return count;
}
/**
* Checks if a given number marks the start of a new page.
*
* @param {number} num - The number to check.
* @return {boolean} True if the number starts a new page, otherwise false.
*/
function isNewPage (num) { return num % 25 === 1 }
/**
* Determines if a given number is 'nice' (ends with 69).
*
* @param {number} num - The number to check.
* @return {boolean} True if the number is 'nice', otherwise false.
*/
function isNice (num) { return num.toString().slice(-2) === '69' }
/**
* Determines if a given number is a 'Bond' number (ends with 007).
*
* @param {number} num - The number to check.
* @return {boolean} True if the number is a 'Bond' number, otherwise false.
*/
function isBond (num) { return num.toString().slice(-3) === '007' }
/**
* Determines if a number is 'sexy' based on specific criteria.
* A number is considered sexy if it meets any of the following conditions:
* - It is a palindrome.
* - Its digits are in sequential order, either increasing or decreasing.
* - It has a repeating pattern (applicable for even-length numbers).
* - The first several digits repeat in a sequence.
*
* @param {number} num - The number to check.
* @return {boolean} True if the number is sexy, false otherwise.
*/
function isSexy (num) {
// Palindrome
if (isPalindrome(num)) return true;
// Check if the digits are sequentially increasing or decreasing.
const numArr = num.toString().split('').map(i => parseInt(i));
if (!isNaN(numArr.reduce((a, b) => a + 1 === b ? b : NaN)) || !isNaN(numArr.reverse().reduce((a, b) => a + 1 === b ? b : NaN))) return true;
// Check for repeating sequences in the number.
let j = null;
let recurrences = 1;
for (const i of numArr) {
if (j === null) j = i
else if (i === j) {j = i; recurrences++}
else break;
}
// Check if there's a repeating pattern in the number (for even-length numbers).
if (length % 2 === 0 && recurrences > 1 && numArr.map((e, i) => i % recurrences === 0 ? numArr.slice(i, i + recurrences) : null).filter(e => e).map(e => new Set(e).size).find(e => e > 1) === undefined) return true;
}
// =========================================
// LIVE PREVIEW
// =========================================
/**
* Instead of replacing the editor with the preview, we show the preview above the editor.
*/
function ModdedQuick_Preview() {
$.post($bbcodePreview, $("#quickpostform").serialize(), function(e) {
document.getElementById("quickreplypreview").innerHTML = e;
document.getElementById("quickreplypreview").style.display = "block";
});
}
unsafeWindow.Orig_Quick_Preview = Quick_Preview;
unsafeWindow.Quick_Preview = ModdedQuick_Preview;
var throttledUpdatePreview = throttle(function() {
ModdedQuick_Preview();
}, 1000);
document.getElementById('quickpost').addEventListener('input', throttledUpdatePreview);
// =========================================
// START
// =========================================
if (!config.onLastPage) {
print('%cNot on last page', 'color: orange;');
config.autoReload.enabled = false;
config.autoReload.scrollToInput = false;
config.autoReload.preserveContent = false;
config.autoFill = false;
config.showContextNumberList = false;
config.quicksendButton = false;
config.autoSend = false;
}
if (!config.autoFill) {
config.autoReload.preserveContent = false;
}
if (!config.autoReload.enabled) {
config.autoReload.scrollToInput = false;
config.autoReload.preserveContent = false;
}
if (!config.autoSend) {
config.autoSendSpecialOnly = false;
}
printConfig();
addDebugger();
setAutosendIcon();
autoFixWrongNumbered();
checkForUpdates();
autoFill();
addContextNumbers();
canQuicksend();
autoSizeTextArea();
// Call the function periodically to update the time
setInterval(updateRemainingTime, 1000); // Update every second
// Live preview
throttledUpdatePreview();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment