// ==UserScript==
// @name IndexedRelatedTags
// @namespace https://github.com/BrokenEagle/JavaScripts
// @version 3.10
// @description Uses Indexed DB for autocomplete, plus caching of other data.
// @source https://danbooru.donmai.us/users/23799
// @author BrokenEagle
// @match https://*.donmai.us/*
// @exclude /^(?!https:\/\/\w+\.donmai\.us\/((posts|upload_media_assets|uploads)\/\d+|uploads\/\d+\/assets\/\d+|settings)\/?(\?|$)).*/
// @exclude /^https://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/
// @run-at document-idle
// @downloadURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/IndexedRelatedTags.user.js
// @updateURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/IndexedRelatedTags.user.js
// @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js
// @require https://cdn.jsdelivr.net/npm/localforage-removeitems@1.4.0/dist/localforage-removeitems.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/module.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/debug.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/utility.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/validate.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/storage.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/notice.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/concurrency.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/statistics.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/network.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/danbooru.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/load.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20251218/lib/menu.js
// ==/UserScript==
/* global JSPLib $ Danbooru validate */
/****Library updates****/
JSPLib.danbooru.initializeAutocomplete = function (selector, autocomplete_type) {
let $fields = $(selector);
JSPLib.utility.setDataAttribute($fields, 'autocomplete', autocomplete_type);
if (['tag-edit', 'tag-query'].includes(autocomplete_type)) {
$fields.autocomplete({
select (_event, ui) {
Danbooru.Autocomplete.insert_completion(this, ui.item.value);
return false;
},
async source(_request, respond) {
let term = Danbooru.Autocomplete.current_term(this.element);
let results = await Danbooru.Autocomplete.autocomplete_source(term, 'tag_query');
respond(results);
},
});
} else {
let query_type = autocomplete_type.replaceAll(/-/g, '_');
Danbooru.Autocomplete.initialize_fields($fields, query_type);
}
};
/****Global variables****/
//Exterior script variables
const DANBOORU_TOPIC_ID = null;
//Variables for load.js
const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery', 'window.Danbooru', 'Danbooru.RelatedTag', 'Danbooru.CurrentUser', 'Danbooru.Post'];
const PROGRAM_LOAD_REQUIRED_SELECTORS = ['#top', '#page'];
//Program name constants
const PROGRAM_SHORTCUT = 'irt';
const PROGRAM_NAME = 'IndexedRelatedTags';
//Program data constants
const PROGRAM_DATA_REGEX = /^(rt[fcjo](gen|char|copy|art)?|wpt|tagov)-/; //Regex that matches the prefix of all program cache data
//Main program variables
const IRT = {};
//Available setting values
const RELATED_QUERY_ORDERS = ['frequency', 'cosine', 'jaccard', 'overlap'];
const RELATED_QUERY_CATEGORIES = {
general: 0,
copyright: 3,
character: 4,
artist: 1,
meta: 5
};
const RELATED_CATEGORY_NAMES = Object.keys(RELATED_QUERY_CATEGORIES);
//Main settings
const SETTINGS_CONFIG = {
related_query_categories: {
allitems: RELATED_CATEGORY_NAMES,
reset: RELATED_CATEGORY_NAMES,
validate: (data) => JSPLib.menu.validateCheckboxRadio(data, 'checkbox', RELATED_CATEGORY_NAMES),
hint: "Select the category query buttons to show.",
},
related_results_limit: {
reset: 0,
parse: parseInt,
validate: (data) => JSPLib.menu.validateNumber(data, true, 0, 50),
hint: "Number of results to show (1 - 50) for the primary Tags column. Setting to 0 uses Danbooru's default limit."
},
related_query_order_enabled: {
reset: false,
validate: JSPLib.utility.isBoolean,
hint: "Show controls that allow for alternate query orders on related tags."
},
related_query_order_default: {
allitems: RELATED_QUERY_ORDERS,
reset: ['frequency'],
validate: (data) => JSPLib.menu.validateCheckboxRadio(data, 'radio', RELATED_QUERY_ORDERS),
hint: "Select the default query order selected on the related tag controls. Will be the order used when the order controls are not available."
},
expandable_related_section_enabled: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Places all related tag columns on the same row, with top/bottom scrollbars and arrow keys to support scrolling."
},
related_statistics_enabled: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Show tag overlap statistics for related tag results (Tags column only)."
},
random_post_batches: {
reset: 4,
parse: parseInt,
validate: (data) => JSPLib.menu.validateNumber(data, true, 1, 10),
hint: "Number of consecutive queries for random posts (1 - 10)."
},
random_posts_per_batch: {
reset: 100,
parse: parseInt,
validate: (data) => JSPLib.menu.validateNumber(data, true, 20, 200),
hint: "Number of posts to query for each batch (20 - 200)."
},
wiki_page_tags_enabled: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Include wiki page tags when using one of the related tags buttons."
},
wiki_page_query_only_enabled: {
reset: false,
validate: JSPLib.utility.isBoolean,
hint: "Include a button to query only wiki page tags."
},
checklist_tags_enabled: {
reset: false,
validate: JSPLib.utility.isBoolean,
hint: "Include checklist tags when using one of the related tags buttons."
},
checklist_query_only_enabled: {
reset: false,
validate: JSPLib.utility.isBoolean,
hint: "Include a button to add only checklist tags."
},
query_unknown_tags_enabled: {
reset: false,
validate: JSPLib.utility.isBoolean,
hint: "Do an additional query if any wiki page tags are not found with the initial query."
},
other_wikis_enabled: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Include list_of_* wikis when including wiki page tags."
},
unique_wiki_tags_enabled: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Only show one instance of a tag by its first occurrence."
},
recheck_data_interval: {
reset: 1,
parse: parseInt,
validate: (data) => JSPLib.menu.validateNumber(data, true, 0, 3),
hint: "Number of days (0 - 3). Setting to 0 disables this."
},
network_only_mode: {
reset: false,
validate: JSPLib.utility.isBoolean,
hint: `Always goes to network. Warning: This negates the benefit of cached data!`
},
};
//Available config values
const ALL_SOURCE_TYPES = ['indexed_db', 'local_storage'];
const ALL_DATA_TYPES = ['related_tag', 'wiki_page', 'tag_overlap', 'custom'];
const ALL_RELATED = ["", 'general', 'copyright', 'character', 'artist'];
const ALL_ORDER = ['frequent', 'cosine', 'jaccard', 'overlap'];
const CONTROL_CONFIG = {
cache_info: {
value: "Click to populate",
hint: "Calculates the cache usage of the program and compares it to the total usage.",
},
purge_cache: {
display: `Purge cache (...)`,
value: "Click to purge",
hint: `Dumps all of the cached data related to ${PROGRAM_NAME}.`,
},
data_source: {
allitems: ALL_SOURCE_TYPES,
value: 'indexed_db',
hint: "Indexed DB is Cache Data and Local Storage is Program Data.",
},
data_type: {
allitems: ALL_DATA_TYPES,
value: 'related_tag',
hint: "Select type of data. Use Custom for querying by keyname.",
},
tag_category: {
allitems: ALL_RELATED,
value: "",
hint: "Select type of tag category. Blank selects uncategorized data.",
},
query_order: {
allitems: ALL_ORDER,
value: 'frequent',
hint: "Select type of query order.",
},
raw_data: {
value: false,
hint: "Select to import/export all program data",
},
data_name: {
value: "",
buttons: ['get', 'save', 'delete', 'list', 'refresh'],
hint: "Click Get to see the data, Save to edit it, and Delete to remove it. List shows keys in their raw format, and Refresh checks the keys again.",
},
import_export: {
display: 'Import/Export',
value: false,
hint: "Once selected, all checklists can be exported by clicking View, or imported by clicking Save.",
},
tag_name: {
value: "",
buttons: ['view', 'save', 'populate', 'list'],
hint: "Click View to see the list of tags, and Save to commit the changes. Populate will query the current list of wiki page tags. List will show all tags with checklists in alphabetical order.",
},
};
const MENU_CONFIG = {
topic_id: DANBOORU_TOPIC_ID,
settings: [{
name: 'general',
}, {
name: 'related-tag',
message: "Affects the related tags shown in the post/upload edit menu.",
}, {
name: 'tag-statistic',
message: "Shows much overlap there is between the tags in the related tag column and the query term. This does not include wiki page tags.",
}, {
name: 'checklist',
message: "Allows frequent tags on a per-tag basis.",
}, {
name: 'wiki-page',
message: "Affects how the wiki pages get queried for tags.",
}, {
name: 'network',
}],
controls: [{
name: 'checklist',
message: "View and edit frequent tags on a per-tag basis."
}],
};
//Pre-CSS/HTML constants
const DEPRECATED_TAG_CATEGORY = 200;
const NONEXISTENT_TAG_CATEGORY = 300;
//CSS Constants
const PROGRAM_CSS = `
/**Container**/
div#edit-dialog div#irt-related-tags-container {
max-height: 400px;
overflow-y: auto;
}
div#irt-related-tags {
overflow-x: hidden;
flex-wrap: nowrap;
max-width: calc(100% - 2em);
display: inline-flex;
}
div#irt-related-tags > div {
display: inline-flex;
}
div#irt-related-tags.scrollable {
overflow-x: scroll;
}
/**Related tag columns**/
div.irt-tag-column {
width: 18em;
max-width: unset;
margin-right: 1em;
}
div.irt-tag-column.irt-is-empty-true {
display: none;
}
/**Related tag**/
div.irt-no-percentage {
text-indent: -1.5em;
margin-left: 1.5em;
}
div.irt-has-percentage {
text-indent: -3.3em;
margin-left: 3.3em;
}
span.irt-tag-statistic {
filter: hue-rotate(-30deg);
}
span.irt-tag-statistic.irt-high-percent {
letter-spacing: -2px;
}
div.irt-related-tag li {
display: inline;
}
div.irt-related-tag li:before {
content: "*";
font-family: monospace;
font-weight: bold;
visibility: hidden;
padding-right: 0.2em;
}
div.irt-related-tag li.irt-selected:before {
visibility: visible;
}
div.irt-related-tag li.irt-selected {
font-weight: bold;
}
/**Related query controls**/
#irt-related-tag-query-controls {
display: flex;
flex-wrap: wrap;
}
/****Category****/
#irt-related-query-category {
display: inline-flex;
margin-bottom: 0.5em;
margin-right: 1em;
}
#irt-related-query-category .irt-related-button {
margin: 0 2px;
}
#irt-related-query-category .irt-related-button[disabled] {
cursor: default;
}
/****Wiki****/
#irt-wiki-page-controls {
display: inline-flex;
margin-bottom: 0.5em;
margin-right: 1em;
}
#irt-wiki-page-query {
color: white;
font-weight: bold;
border: 1px solid;
margin-right: 0.5em;
}
/****Checklist****/
#irt-checklist-controls:not(.jsplib-controls-grouping) {
display: inline-flex;
margin-bottom: 0.5em;
margin-right: 1em;
}
#irt-checklist-query {
color: white;
font-weight: bold;
border: 1px solid;
margin-right: 0.5em;
}
/****Query type****/
#irt-related-query-type {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 0.5em;
}
#irt-related-query-type label {
cursor: pointer;
user-select: none;
display: block;
font-size: 14px;
line-height: 1.5em;
font-weight: bold;
border: 1px solid;
border-radius: 3px;
padding: 2px 8px;
text-align: center;
}
#irt-related-query-type input {
margin-left: 0.25em;
vertical-align: middle;
cursor: pointer;
}
/**Expandable sections**/
#irt-edit-scroll-wrapper {
height: 20px;
overflow-x: scroll;
overflow-y: hidden;
display: none;
}
#irt-edit-scroll-bar {
height: 20px;
}`;
const LIGHT_MODE_CSS = `
/**Related tag**/
span.irt-tag-statistic {
color: var(--red-3);
}
span.irt-tag-statistic.irt-low-percent {
color: var(--grey-3);
}
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:link,
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:visited {
color: var(--yellow-3);
}
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:hover {
color: var(--yellow-2);
}
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:link,
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:visited {
color: var(--grey-4);
}
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:hover {
color: var(--grey-3);
}
/**Related query controls**/
/****Wiki****/
#irt-wiki-page-query {
background-color: var(--green-4);
border-color: var(--green-5);
}
/****Checklist****/
#irt-checklist-query {
background-color: var(--orange-3);
border-color: var(--orange-4);
}
/****Query type****/
#irt-related-query-type label {
color: var(--black);
background-color: var(--grey-2);
border-color: var(--grey-3);
}`;
const DARK_MODE_CSS = `
/**Related tag**/
span.irt-tag-statistic {
color: var(--red-5);
}
span.irt-tag-statistic.irt-low-percent {
color: var(--grey-5);
}
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:link,
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:visited {
color: var(--orange-5);
}
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:hover {
color: var(--orange-4);
}
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:link,
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:visited {
color: var(--grey-5);
}
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:hover {
color: var(--grey-4);
}
/**Related query controls**/
/****Wiki****/
#irt-wiki-page-query {
background-color: var(--green-5);
border-color: var(--green-6);
}
/****Checklist****/
#irt-checklist-query {
background-color: var(--orange-5);
border-color: var(--orange-6);
}
/****Query type****/
#irt-related-query-type label {
color: var(--white);
background-color: var(--grey-6);
border-color: var(--grey-7);
}`;
const MENU_CSS = `
#indexed-related-tags .jsplib-settings-grouping:not(#irt-general-settings) .irt-selectors label {
width: 150px;
}
#indexed-related-tags .irt-sortlist li {
width: 10em;
}
#indexed-related-tags .irt-formula {
font-family: mono;
}
#irt-checklist-frequent-tags textarea {
width: 40em;
height: 25em;
}`;
//HTML Constants
const RELATED_TAG_SETTINGS_DETAILS = `
Related query orders:
Frequency: Uses the frequency of tags that appear with the queried tag from a sample of 1000 posts.
Cosine: The overall similarity of tags, regardless of their post count or overlap.
Jaccard: The specific similarity of tags, taking into account the post count and overlap.
Overlap: The number of posts tags have in common.
Note: Each related query order is stored separately, so results can be repeated with different values.
`;
const NETWORK_SETTINGS_DETAILS = `
Recheck data interval: Data expiring within this period gets automatically requeried.
Network only mode:
Can be used to correct cache data that has been changed on the server.
Warning!As this negates the benefits of using local cached data, it should only be used sparingly.
`;
const CACHE_DATA_DETAILS = `
Related tag data: Frequency tags from each of the tag categories (beneath the tag edit box).
all (rt)
general (rtgen)
artists (rtart)
characters (rtchar)
copyrights (rtcopy)
meta (rtmeta)
Wiki page tags (wpt): Wiki links to other tags.
Tags overlap (tagov): Frequency of tag in relation to other tags.
`;
const PROGRAM_DATA_DETAILS = `
All timestamps are in milliseconds since the epoch (Epoch converter).
General data
prune-expires: When the program will next check for cache data that has expired.
user-settings: All configurable settings.
`;
const CHECKLIST_TEXTAREA = `
`;
const IRT_SCROLL_WRAPPER = `
`;
const IRT_RELATED_TAGS_SECTION = `
`;
const WIKI_PAGE_BUTTON = `
`;
const CHECKLIST_BUTTON = `
`;
//Time constants
const PRUNE_EXPIRES = JSPLib.utility.one_day;
//Expiration variables
const TAGS_OVERLAP_EXPIRES = JSPLib.utility.one_month;
const WIKI_PAGE_TAGS_EXPIRES = 2 * JSPLib.utility.one_week;
const RELATED_TAG_EXPIRES = JSPLib.utility.one_week;
//Validate constants
const RELATEDTAG_CONSTRAINTS = {
entry: JSPLib.validate.hashentry_constraints,
value: {
categories: JSPLib.validate.array_constraints,
query: JSPLib.validate.stringonly_constraints,
tags: JSPLib.validate.tagentryarray_constraints(),
},
categories: JSPLib.validate.inclusion_constraints(ALL_RELATED),
};
const TAG_OVERLAP_CONSTRAINTS = {
entry: JSPLib.validate.hashentry_constraints,
value: {
count: JSPLib.validate.counting_constraints,
overlap: JSPLib.validate.hash_constraints,
},
overlap: JSPLib.validate.basic_integer_validator,
};
const WIKI_PAGE_CONSTRAINTS = {
entry: JSPLib.validate.hashentry_constraints,
value: {
title: JSPLib.validate.stringonly_constraints,
tags: JSPLib.validate.tagentryarray_constraints([0, 1, 3, 4, 5, NONEXISTENT_TAG_CATEGORY]),
other_wikis: JSPLib.validate.array_constraints,
},
other_wikis: JSPLib.validate.basic_stringonly_validator,
};
/****Functions****/
//Validate functions
function ValidateEntry(key, entry) {
const printer = JSPLib.debug.getFunctionPrint('ValidateEntry');
if (!JSPLib.validate.validateIsHash(key, entry)) {
return false;
}
if (key.match(/^rt[fcjo](gen|char|copy|art)?-/)) {
return ValidateRelatedtagEntry(key, entry);
}
if (key.match(/^tagov-/)) {
return ValidateTagOverlapEntry(key, entry);
}
if (key.match(/^wpt-/)) {
return ValidateWikiPageEntry(key, entry);
}
printer.debuglog("Bad key!");
return false;
}
function ValidateRelatedtagEntry(key, entry) {
if (!JSPLib.validate.validateHashEntries(key, entry, RELATEDTAG_CONSTRAINTS.entry)) {
return false;
}
if (!JSPLib.validate.validateHashEntries(key + '.value', entry.value, RELATEDTAG_CONSTRAINTS.value)) {
return false;
}
return true;
}
function ValidateTagOverlapEntry(key, entry) {
if (!JSPLib.validate.validateHashEntries(key, entry, TAG_OVERLAP_CONSTRAINTS.entry)) {
return false;
}
if (!JSPLib.validate.validateHashEntries(key + '.value', entry.value, TAG_OVERLAP_CONSTRAINTS.value)) {
return false;
}
if (!JSPLib.validate.validateHashValues(key + '.value.overlap', entry.value.overlap, TAG_OVERLAP_CONSTRAINTS.overlap)) {
return false;
}
return true;
}
function ValidateWikiPageEntry(key, entry) {
if (!JSPLib.validate.validateHashEntries(key, entry, WIKI_PAGE_CONSTRAINTS.entry)) {
return false;
}
if (!JSPLib.validate.validateHashEntries(key + '.value', entry.value, WIKI_PAGE_CONSTRAINTS.value)) {
return false;
}
if (!JSPLib.validate.validateArrayValues(key + '.other_wikis', entry.value.other_wikis, WIKI_PAGE_CONSTRAINTS.other_wikis)) {
return false;
}
return true;
}
function ValidateProgramData(key, entry) {
var checkerror = [];
switch (key) {
case 'irt-user-settings':
checkerror = JSPLib.menu.validateUserSettings(entry, SETTINGS_CONFIG);
break;
case 'irt-prune-expires':
if (!Number.isInteger(entry)) {
checkerror = ["Value is not an integer."];
}
break;
default:
checkerror = ["Not a valid program data key."];
}
if (checkerror.length) {
JSPLib.validate.outputValidateError(key, checkerror);
return false;
}
return true;
}
//Auxiliary functions
function GetRelatedKeyModifer(category, query_order) {
return 'rt' + query_order[0] + (category ? JSPLib.danbooru.getShortName(category) : "");
}
function FilterTagEntries(tagentries) {
if (!IRT.unique_wiki_tags_enabled) return tagentries;
let tags_seen = new Set();
return tagentries.filter((entry) => {
if (tags_seen.has(entry[0])) return false;
tags_seen.add(entry[0]);
return true;
});
}
function GetTagsEntryArray(wiki_page) {
let wiki_link_targets = JSPLib.utility.findAll(wiki_page.body, /\[\[([^|\]]+)\|?[^\]]*\]\]/g)
.filter((str) => !str.startsWith('[['))
.map((str) => str.toLowerCase()
.replace(/ /g, '_')
.replace(/_+/g, '_')
.replace(/^_+/g, "")
.replace(/_+$/g, ""))
.filter((str) => !str.match(/^(?:tag_group|pool_group|help|howto|about|template|disclaimer):|list_of_/));
return wiki_link_targets.map((link_target) => {
let dtext_link = (wiki_page.dtext_links || []).find((dtext_link) => dtext_link.link_target === link_target);
if (dtext_link) {
if (dtext_link.linked_tag?.is_deprecated) {
return [link_target, DEPRECATED_TAG_CATEGORY];
}
if (dtext_link.linked_tag) {
return [link_target, dtext_link.linked_tag.category];
}
return [link_target, NONEXISTENT_TAG_CATEGORY];
}
return null;
}).filter((tag_entry) => tag_entry !== null);
}
function GetChecklistTagsArray(tag_name) {
let tag_array = JSPLib.storage.getLocalData('irt-checklist-' + tag_name, {default_val: []});
let check = validate({tag_array}, {tag_array: JSPLib.validate.tagentryarray_constraints([0, 1, 3, 4, 5, DEPRECATED_TAG_CATEGORY, NONEXISTENT_TAG_CATEGORY])});
if (check) {
console.warn(`Validation error[${tag_name}]:`, check, tag_array);
return null;
}
return tag_array;
}
function CreateTagArray(tag_list, tag_data) {
return tag_list.map((name) => {
let tag = tag_data.find((item) => item.name === name);
if (!tag) {
return [name, NONEXISTENT_TAG_CATEGORY];
}
if (tag.is_deprecated) {
return [name, DEPRECATED_TAG_CATEGORY];
}
return [name, tag.category];
});
}
function GetTagQueryParams(tag_list) {
return {
search: {
name_comma: tag_list.join(',')
},
only: 'name,category,is_deprecated',
limit: tag_list.length
};
}
//Render functions
function RenderTaglist(taglist, columnname, tags_overlap) {
let html = "";
let display_percentage = Boolean(IRT.related_statistics_enabled && JSPLib.utility.isHash(tags_overlap));
taglist.forEach((tagdata) => {
let tag = tagdata[0];
let escaped_tag = JSPLib.utility.HTMLEscape(tag);
let category = tagdata[1];
let display_name = tag.replace(/_/g, ' ');
let search_link = JSPLib.danbooru.postSearchLink(tag, display_name, `class="search-tag" data-category="${category}" data-tag-name="${escaped_tag}"`);
var prefix, classname;
if (display_percentage) {
var percentage_string, percent_classname;
if (Number.isInteger(tags_overlap.overlap[tag])) {
let tag_percentage = Math.ceil(100 * (tags_overlap.overlap[tag] / tags_overlap.count)) || 0;
percentage_string = JSPLib.utility.padNumber(tag_percentage, 2) + '%';
percent_classname = (tag_percentage >= 100 ? 'irt-high-percent' : "");
} else {
percentage_string = ">5%";
percent_classname = 'irt-low-percent';
}
prefix = `${percentage_string} `;
classname = 'irt-has-percentage';
} else {
prefix = "";
classname = 'irt-no-percentage';
}
var title;
if (category === DEPRECATED_TAG_CATEGORY) {
title = 'deprecated';
} else if (category === NONEXISTENT_TAG_CATEGORY) {
title = 'nonexistent';
} else {
title = "";
}
html += `
${prefix}
${search_link}
`;
});
return `
${columnname}
${html}
`;
}
function RenderTagColumn(classname, column_html, is_empty) {
return `
${column_html}
`;
}
function RenderTagQueryColumn(related_tags, tags_overlap) {
let is_empty = related_tags.tags.length === 0;
let display_name = related_tags.query.replace(/_/g, ' ');
let column_html = (!is_empty ? RenderTaglist(related_tags.tags, display_name, tags_overlap) : "");
return RenderTagColumn('irt-general-related-tags-column', column_html, is_empty);
}
function RenderChecklistColumn(checklist_tags, tag_name) {
let is_empty = checklist_tags.length === 0;
let display_name = "Checklist: " + tag_name.replace(/_/g, ' ');
let column_html = (!is_empty ? RenderTaglist(checklist_tags, display_name) : "");
return RenderTagColumn('irt-checklist-related-tags-column', column_html, is_empty);
}
function RenderWikiTagQueryColumns(wiki_page, other_wikis) {
let is_empty = wiki_page.tags.length === 0;
let display_name = wiki_page.title.replace(/_/g, ' ');
let column_html = (!is_empty ? RenderTaglist(FilterTagEntries(wiki_page.tags), JSPLib.danbooru.wikiLink(wiki_page.title, `wiki:${display_name}`, 'target="_blank"')) : "");
let html = RenderTagColumn('irt-wiki-related-tags-column', column_html, is_empty);
other_wikis.forEach((other_wiki) => {
if (other_wiki.tags.length === 0) return;
let title_name = other_wiki.title.replace(/_/g, ' ');
column_html = RenderTaglist(FilterTagEntries(other_wiki.tags), JSPLib.danbooru.wikiLink(other_wiki.title, `wiki:${title_name}`, 'target="_blank"'));
html += RenderTagColumn('irt-wiki-related-tags-column', column_html, false);
});
return html;
}
function RenderUserQueryColumns(recent_tags, frequent_tags, ai_tags) {
let is_empty = recent_tags.length === 0;
let column_html = (is_empty ? "" : RenderTaglist(recent_tags, 'Recent'));
let html = RenderTagColumn('irt-recent-related-tags-column', column_html, is_empty);
is_empty = frequent_tags.length === 0;
column_html = (!is_empty ? RenderTaglist(frequent_tags, 'Frequent') : "");
html += RenderTagColumn('irt-frequent-related-tags-column', column_html, is_empty);
is_empty = ai_tags.length === 0;
column_html = (!is_empty ? RenderTaglist(ai_tags, 'Suggested') : "");
html += RenderTagColumn('irt-ai-tags-related-tags-column', column_html, is_empty);
return html;
}
function RenderTranslatedColumn(translated_tags) {
let is_empty = translated_tags.length === 0;
let column_html = (!is_empty ? RenderTaglist(translated_tags, 'Translated') : "");
return RenderTagColumn('irt-translated-tags-related-tags-column', column_html, is_empty);
}
function RenderRelatedQueryCategoryControls() {
let html = '';
for (let category in RELATED_QUERY_CATEGORIES) {
if (!IRT.related_query_categories.includes(category)) continue;
let display_name = JSPLib.utility.displayCase(category);
html += `
`;
}
return `
${html}
`;
}
function RenderRelatedQueryTypeControls() {
let html = "";
RELATED_QUERY_ORDERS.forEach((type) => {
let checked = (IRT.related_query_order_default[0] === type ? 'checked' : "");
let display_name = JSPLib.utility.displayCase(type);
html += `
`;
});
return `
${html}
`;
}
//Network functions
async function RandomPosts(tag, batches, per_batch) {
let posts = [];
let url_addons = {
tags: tag + ' order:md5', // Gives us quasi-random results
only: 'id,md5,tag_string',
limit: per_batch,
};
for (let i = 1; i <= batches; i++) {
url_addons.page = i;
let result = await JSPLib.danbooru.submitRequest('posts', url_addons);
posts = JSPLib.utility.concat(posts, result);
if (result.length < per_batch) break;
}
return posts;
}
async function TagsOverlapQuery(tag) {
const printer = JSPLib.debug.getFunctionPrint('TagsOverlapQuery');
const [batches, per_batch] = [IRT.random_post_batches, IRT.random_posts_per_batch];
printer.debuglog("Querying:", tag, batches, per_batch);
let [posts, count] = await Promise.all([
RandomPosts(tag, batches, per_batch),
JSPLib.danbooru.submitRequest('counts/posts', {tags: tag}, {default_val: {counts: {posts: 0}}})
]);
let overlap = {};
for (let i = 0; i < posts.length; i++) {
let tag_names = posts[i].tag_string.split(' ');
tag_names.forEach((tag) => {
overlap[tag] = (overlap[tag] ?? 0) + 1;
});
}
let cutoff = Math.min(posts.length, batches * per_batch) / 20; // (5% or greater overlap)
for (let k in overlap) {
if (overlap[k] < cutoff) {
delete overlap[k];
}
}
return {value: {overlap, count: Math.min(count.counts.posts, batches * per_batch)}, expires: JSPLib.utility.getExpires(TAGS_OVERLAP_EXPIRES)};
}
async function WikiPageTagsQuery(title) {
const printer = JSPLib.debug.getFunctionPrint('WikiPageTagsQuery');
printer.debuglog("Querying:", title, (IRT.other_wikis_enabled ? "with" : "without", "other wikis"));
let url_addons = {
search: {title},
only: 'body,tag,dtext_links[link_target,link_type,linked_tag[name,category,is_deprecated]]'
};
let wikis_with_links = await JSPLib.danbooru.submitRequest('wiki_pages', url_addons);
let tags = (wikis_with_links.length ? GetTagsEntryArray(wikis_with_links[0]) : []);
if (IRT.query_unknown_tags_enabled) {
let tag_names = tags.filter((tag) => tag[1] === NONEXISTENT_TAG_CATEGORY).map((tag) => tag[0]);
if (tag_names.length) {
let tag_data = await JSPLib.danbooru.submitRequest('tags', GetTagQueryParams(tag_names));
let tag_array = CreateTagArray(tag_names, tag_data);
tags = tags.map((tag_entry) => (tag_array.find((item) => item[0] === tag_entry[0]) ?? tag_entry));
}
}
let other_wikis = (!title.startsWith('list_of_') ?
(wikis_with_links?.[0]?.dtext_links || [])
.filter((link) => (link.link_type === 'wiki_link' && link.link_target.startsWith('list_of_')))
.map((link) => link.link_target) :
[]);
return {value: {title, tags, other_wikis}, expires: JSPLib.utility.getExpires(WIKI_PAGE_TAGS_EXPIRES)};
}
async function RelatedTagsQuery(tag, category, query_order) {
const printer = JSPLib.debug.getFunctionPrint('RelatedTagsQuery');
printer.debuglog("Querying:", tag, category);
let url_addons = {search: {query: tag, order: query_order}, limit: IRT.related_results_limit || Danbooru.RelatedTag.MAX_RELATED_TAGS};
if (category in RELATED_QUERY_CATEGORIES) {
url_addons.search.category = RELATED_QUERY_CATEGORIES[category];
}
let html = await JSPLib.network.get('/related_tag.html', {data: url_addons});
let tagentry_array = $(html).find('tbody .name-column a[href^="/posts"]').toArray().map((link) => {
let name = link.innerText;
let category = Number(
link.className
.match(/tag-type-(\d)/)?.[1]
?? NONEXISTENT_TAG_CATEGORY
);
return [name, category];
});
let data = {
query: tag,
categories: (category ? [RELATED_QUERY_CATEGORIES[category]] : []),
tags: tagentry_array,
};
return {value: data, expires: JSPLib.utility.getExpires(RELATED_TAG_EXPIRES)};
}
//Network/storage wrappers
async function GetCachedData({name = "", args = [], keyfunc = (() => {}), netfunc = (() => {}), expires = null} = {}) {
const printer = JSPLib.debug.getFunctionPrint('GetCachedData');
let key = keyfunc(...args);
printer.debuglog("Checking", name, ':', key);
let cached = await (!IRT.network_only_mode ?
JSPLib.storage.checkLocalDB(key, expires) :
Promise.resolve(null));
if (!cached) {
cached = await netfunc(...args);
JSPLib.storage.saveData(key, cached);
} else if (IRT.recheck_data_interval > 0) {
let recheck_time = cached.expires - (IRT.recheck_data_interval * JSPLib.utility.one_day);
if (!JSPLib.utility.validateExpires(recheck_time)) {
printer.debuglog("Rechecking", name, key);
netfunc(...args).then((data) => {
JSPLib.storage.saveData(key, data);
});
}
}
printer.debuglog("Found", name, ':', key, cached.value);
return cached.value;
}
function GetRelatedTags(tag, category, query_order) {
return GetCachedData({
name: 'related tags',
args: [tag, category, query_order],
keyfunc: (tag, category, query_order) => (GetRelatedKeyModifer(category, query_order) + '-' + tag),
netfunc: RelatedTagsQuery,
expires: RELATED_TAG_EXPIRES,
});
}
function GetTagsOverlap(tag) {
return GetCachedData({
name: 'tags overlap',
args: [tag],
keyfunc: (tag) => ('tagov-' + tag),
netfunc: TagsOverlapQuery,
expires: TAGS_OVERLAP_EXPIRES,
});
}
function GetWikiPageTags(tag) {
return GetCachedData({
name: 'wiki page tags',
args: [tag],
keyfunc: (tag) => ('wpt-' + tag),
netfunc: WikiPageTagsQuery,
expires: WIKI_PAGE_TAGS_EXPIRES,
});
}
async function GetAllWikiPageTags(tag) {
let wiki_page = await GetWikiPageTags(tag);
var other_wikis;
if (IRT.other_wikis_enabled) {
let promise_array = [];
wiki_page.other_wikis.forEach((title) => {
promise_array.push(GetWikiPageTags(title));
});
other_wikis = await Promise.all(promise_array);
} else {
other_wikis = [];
}
return {wiki_page, other_wikis};
}
//Event handlers
async function RelatedTagsButton(event) {
event.preventDefault();
let currenttag = Danbooru.RelatedTag.current_tag().trim().toLowerCase();
let category = $(event.target).data('selector');
let query_order = (IRT.related_query_order_enabled ? JSPLib.menu.getCheckboxRadioSelected('.irt-program-checkbox') : IRT.related_query_order_default);
let promise_array = [GetRelatedTags(currenttag, category, query_order[0])];
if (IRT.related_statistics_enabled) {
promise_array.push(GetTagsOverlap(currenttag));
} else {
promise_array.push(Promise.resolve(null));
}
if (IRT.wiki_page_tags_enabled) {
promise_array.push(GetAllWikiPageTags(currenttag));
} else {
promise_array.push(Promise.resolve(null));
}
let [related_tags, tags_overlap, wiki_result] = await Promise.all(promise_array);
if (!related_tags) {
return;
}
var tag_array;
if (IRT.checklist_tags_enabled) {
tag_array = GetChecklistTagsArray(currenttag) ?? [];
}
$('#irt-related-tags-query-container').html(
RenderTagQueryColumn(related_tags, tags_overlap) +
(IRT.checklist_tags_enabled ? RenderChecklistColumn(tag_array, currenttag) : "") +
(IRT.wiki_page_tags_enabled ? RenderWikiTagQueryColumns(wiki_result.wiki_page, wiki_result.other_wikis) : "")
);
UpdateSelected();
QueueRelatedTagColumnWidths();
}
async function WikiPageButton(event) {
event.preventDefault();
let currenttag = Danbooru.RelatedTag.current_tag().trim().toLowerCase();
let wiki_result = await GetAllWikiPageTags(currenttag);
$('#irt-related-tags-query-container').html(RenderWikiTagQueryColumns(wiki_result.wiki_page, wiki_result.other_wikis));
UpdateSelected();
QueueRelatedTagColumnWidths();
}
function ChecklistButton(event) {
event.preventDefault();
let currenttag = Danbooru.RelatedTag.current_tag().trim().toLowerCase();
let tag_array = GetChecklistTagsArray(currenttag);
if (tag_array === null) {
JSPLib.notice.error("Corrupted data: See debug console for details.");
} else {
$('#irt-related-tags-query-container').html(RenderChecklistColumn(tag_array, currenttag));
UpdateSelected();
QueueRelatedTagColumnWidths();
}
}
function RelatedTagsEnter() {
$(document).on(JSPLib.program_keydown + '.scroll', null, 'left right', RelatedTagsScroll);
}
function RelatedTagsLeave() {
$(document).off(JSPLib.program_keydown + '.scroll');
}
function RelatedTagsScroll(event) {
let $related_tags = $('#irt-related-tags');
let current_left = $related_tags.prop('scrollLeft');
if (event.originalEvent.key === 'ArrowLeft') {
current_left -= 40;
} else if (event.originalEvent.key === 'ArrowRight') {
current_left += 40;
}
$related_tags.prop('scrollLeft', current_left);
}
function ViewChecklistTag() {
let import_export = $('#irt-enable-import-export').prop('checked');
if (import_export) {
let tag_list =
Object.keys(localStorage)
.filter((name) => name.startsWith('irt-checklist-'))
.map((name) => name.replace('irt-checklist-', ""));
let tag_data = tag_list.map((tag_name) => {
let tag_array = GetChecklistTagsArray(tag_name);
if (Array.isArray(tag_array)) {
return {[tag_name]: tag_array.map((item) => item[0])};
}
return null;
}).filter((data) => data != null);
$('#irt-checklist-frequent-tags textarea').val(JSON.stringify(JSPLib.utility.mergeHashes(...tag_data), null, 4));
} else {
let tag_name = $('#irt-control-tag-name').val().split(/\s+/)[0];
if (!tag_name) return;
let tag_array = GetChecklistTagsArray(tag_name);
if (tag_array === null) {
JSPLib.notice.error("Corrupted data: See debug console for details.");
} else {
let tag_list = tag_array.map((entry) => entry[0]);
$('#irt-checklist-frequent-tags textarea').val(tag_list.join('\n'));
}
}
}
async function SaveChecklistTag() {
let import_export = $('#irt-enable-import-export').prop('checked');
if (import_export) {
let text_input = $('#irt-checklist-frequent-tags textarea').val();
var data_input;
try {
data_input = JSON.parse(text_input);
} catch (e) {
data_input = null;
JSPLib.debug.debugerror("Error parsing data:", e);
}
if (JSPLib.utility.isHash(data_input)) {
let checklist_data = {};
let check_tags = [];
for (let key in data_input) {
let checklist = data_input[key];
if (!Array.isArray(checklist)) continue;
checklist = checklist.filter((item) => typeof item === "string");
if (checklist.length === 0) continue;
checklist_data[key] = checklist;
check_tags = JSPLib.utility.arrayUnion(check_tags, checklist);
}
if (check_tags.length > 0) {
JSPLib.notice.notice("Querying tags...");
let tag_data = [];
for (let i = 0; i < check_tags.length; i += 1000) {
let query_tags = check_tags.slice(i, i + 1000);
let tags = await JSPLib.danbooru.submitRequest('tags', GetTagQueryParams(query_tags), {long_format: true});
tag_data = JSPLib.utility.concat(tag_data, tags);
}
for (let tag_name in checklist_data) {
let checklist = checklist_data[tag_name];
let tag_array = CreateTagArray(checklist, tag_data);
JSPLib.storage.setLocalData('irt-checklist-' + tag_name, tag_array);
}
JSPLib.notice.notice("Checklists imported.");
} else {
JSPLib.notice.error("No valid checklists found.");
}
} else {
JSPLib.notice.error("Error importing checklist.");
}
} else {
let tag_name = $('#irt-control-tag-name').val().split(/\s+/)[0];
if (!tag_name) return;
let checklist = $('#irt-checklist-frequent-tags textarea').val().split(/\s/).filter((name) => (name !== ""));
if (checklist.length > 0) {
let tag_data = await JSPLib.danbooru.submitRequest('tags', GetTagQueryParams(checklist));
let tag_array = CreateTagArray(checklist, tag_data);
JSPLib.storage.setLocalData('irt-checklist-' + tag_name, tag_array);
} else {
JSPLib.storage.removeLocalData('irt-checklist-' + tag_name);
}
JSPLib.notice.notice("Checklist updated.");
}
}
function PopulateChecklistTag() {
let tag_name = $('#irt-control-tag-name').val().split(/\s+/)[0];
if (!tag_name) return;
JSPLib.notice.notice("Querying Danbooru...");
WikiPageTagsQuery(tag_name).then((data) => {
let tag_list = data.value.tags.map((entry) => entry[0]);
$('#irt-checklist-frequent-tags textarea').val(tag_list.join('\n'));
});
}
function ListChecklistTags() {
let tag_list =
Object.keys(localStorage)
.filter((name) => name.startsWith('irt-checklist-'))
.map((name) => name.replace('irt-checklist-', ""))
.sort();
$('#irt-checklist-frequent-tags textarea').val(tag_list.join('\n'));
}
//Initialization functions
function InitializeUserMediaTags() {
const printer = JSPLib.debug.getFunctionPrint('InitializeUserMediaTags');
let recent_tags = $('.recent-related-tags-column [data-tag-name]').map((_, entry) => [[entry.dataset.tagName, Number(entry.className.match(/tag-type-(\d)/)?.[1])]]).toArray();
let frequent_tags = $('.frequent-related-tags-column [data-tag-name]').map((_, entry) => [[entry.dataset.tagName, Number(entry.className.match(/tag-type-(\d)/)?.[1])]]).toArray();
let ai_tags = $('.ai-tags-related-tags-column [data-tag-name]').map((_, entry) => [[entry.dataset.tagName, Number(entry.className.match(/tag-type-(\d)/)?.[1])]]).toArray();
printer.debuglog("Media tags:", {recent_tags, frequent_tags, ai_tags});
$('#irt-frequent-recent-container').html(RenderUserQueryColumns(recent_tags, frequent_tags, ai_tags));
UpdateSelected();
QueueRelatedTagColumnWidths();
}
function InitializeTranslatedTags() {
const printer = JSPLib.debug.getFunctionPrint('InitializeTranslatedTags');
let translated_tags = $('.translated-tags-related-tags-column [data-tag-name]').map((_, entry) => [[entry.dataset.tagName, Number(entry.className.match(/tag-type-(\d)/)?.[1])]]).toArray();
printer.debuglog("Translated tags:", translated_tags);
$('#irt-translated-tags-container').html(RenderTranslatedColumn(translated_tags));
UpdateSelected();
QueueRelatedTagColumnWidths();
}
function UpdateSelected() {
const current_tags = Danbooru.RelatedTag.current_tags();
$('#irt-related-tags li').each((_, li) => {
const tag_name = $(li).find('a').attr('data-tag-name');
if (current_tags.includes(tag_name)) {
$(li).addClass('irt-selected');
$(li).find('input').prop('checked', true);
} else {
$(li).removeClass('irt-selected');
$(li).find('input').prop('checked', false);
}
});
}
function ToggleTag(event) {
let $field = $('#post_tag_string');
let $link = $(event.target).closest('li').find('a');
let category = $link.data('category');
let tag = $link.data('tag-name');
if (category === DEPRECATED_TAG_CATEGORY) {
JSPLib.notice.error(`Tag "${tag}" is deprecated.`);
event.preventDefault();
return;
}
if (category === NONEXISTENT_TAG_CATEGORY && !Danbooru.RelatedTag.current_tags().includes(tag) && !confirm(`Tag "${tag}" does not exist. Continue?`)) {
event.preventDefault();
return;
}
let old_value = $field.val();
if (Danbooru.RelatedTag.current_tags().includes(tag)) {
let escaped_tag = RegExp.escape(tag);
let regex = new RegExp('(^|\\s)' + escaped_tag + '($|\\s)', 'gi');
let updated_value = old_value.replace(regex, '$1$2')
$field.val(updated_value);
} else {
$field.val(old_value + ' ' + tag);
}
let normalized_value = $field.val().trim().replace(/ +/g, ' ');
$field.val(normalized_value + ' ');
UpdateSelected();
// The timeout is needed on Chrome since it will clobber the field attribute otherwise
setTimeout(() => {
$field.prop('selectionStart', $field.val().length);
}, 100);
event.preventDefault();
// Artificially trigger input event so the tag counter updates.
$field.trigger('input');
}
//Initialize functions
function InitializeRelatedTagsSection() {
Danbooru.Post.EDIT_DIALOG_MIN_HEIGHT = 800;
$(document).on(JSPLib.program_click, '#irt-related-tags a.search-tag', ToggleTag);
$(document).on(JSPLib.program_click, '.irt-related-button', RelatedTagsButton);
$(document).on(JSPLib.program_click, '.irt-wiki-button', WikiPageButton);
$(document).on(JSPLib.program_click, '.irt-checklist-button', ChecklistButton);
$(document).on('input.irt', '#post_tag_string', UpdateSelected);
InitialiazeRelatedQueryControls();
$('.related-tags').before(IRT_RELATED_TAGS_SECTION);
$('#related-tags-container').hide();
InitializeTagColumns();
if (IRT.expandable_related_section_enabled) {
InitialiazeRelatedExpandableSection();
}
}
function InitializeTagColumns() {
const printer = JSPLib.debug.getFunctionPrint('InitializeTagColumns');
if (IRT.controller === 'posts') {
let media_asset_id = $("#related-tags-container").attr("data-media-asset-id");
JSPLib.network.get("/related_tag.js", {data: {user_tags: true, media_asset_id}});
}
if (!$('#related-tags-container .ai-tags-related-tags-column').html()?.trim()) {
printer.debuglog("User/Media tags not loaded yet... setting up mutation observer.");
JSPLib.concurrency.setupMutationReplaceObserver('#related-tags-container', '.ai-tags-related-tags-column', () => {
InitializeUserMediaTags();
});
} else {
InitializeUserMediaTags();
}
if (!$('#related-tags-container .translated-tags-related-tags-column').html()?.trim()) {
printer.debuglog("Translated tags not loaded yet... setting up mutation observer.");
JSPLib.concurrency.setupMutationReplaceObserver('#related-tags-container', '.translated-tags-related-tags-column', () => {
InitializeTranslatedTags();
});
} else {
InitializeTranslatedTags();
}
}
function InitialiazeRelatedQueryControls() {
$('#post_tag_string, #upload_tag_string').parent().after('');
$('#irt-related-tag-query-controls').append(RenderRelatedQueryCategoryControls());
if (IRT.wiki_page_query_only_enabled) {
$('#irt-related-tag-query-controls').append(WIKI_PAGE_BUTTON);
}
if (IRT.checklist_query_only_enabled) {
$('#irt-related-tag-query-controls').append(CHECKLIST_BUTTON);
}
if (IRT.related_query_order_enabled) {
$('#irt-related-tag-query-controls').append(RenderRelatedQueryTypeControls());
$('#irt-related-query-type .ui-state-hover').removeClass('ui-state-hover');
}
$('#post_tag_string').css('max-width', '80rem');
$('#post_tag_string').closest('.fixed-width-container').css('max-width', '80rem');
}
function InitialiazeRelatedExpandableSection() {
$('#irt-related-tags').before(IRT_SCROLL_WRAPPER);
$('#irt-related-tags').on(JSPLib.program_mouseenter, RelatedTagsEnter);
$('#irt-related-tags').on(JSPLib.program_mouseleave, RelatedTagsLeave);
$('#irt-edit-scroll-wrapper').on(JSPLib.program_scroll, () => {
$('#irt-related-tags').scrollLeft($('#irt-edit-scroll-wrapper').scrollLeft());
});
$('#irt-related-tags').on(JSPLib.program_scroll, () => {
$('#irt-edit-scroll-wrapper').scrollLeft($('#irt-related-tags').scrollLeft());
});
let $container = $('#irt-related-tags-container');
new ResizeObserver(() => {
QueueRelatedTagColumnWidths();
}).observe($container[0]);
}
function InitializeRelatedTagColumnWidths() {
const em_size = 14;
const max_column_em = 20;
const min_column_em = 10;
const range = document.createRange();
const getChildWidth = (_, child) => {
if (child.nodeType === 3) {
range.selectNodeContents(child);
const rects = range.getClientRects();
return (rects.length > 0 ? rects[0].width : 0);
}
return $(child).outerWidth();
};
const getSum = (a, b) => (a + b);
let $related_tags = $('#irt-related-tags');
$('.irt-tag-column', $related_tags[0]).each((_, column) => {
let $column = $(column);
$column.css('width', "");
let $container = $('>ul,>div', column);
let $children = $container.children();
if ($children.length === 0) {
return;
}
let line_tag = $container.children().get(0).tagName.toLowerCase();
let container_tag = $container.get(0).tagName.toLowerCase();
let line_selector = container_tag + '>' + line_tag;
let max_child_width = Math.max(...$(line_selector, column).map((_, entry) => {
let child_widths = $(entry).contents().map(getChildWidth).toArray();
return child_widths.reduce(getSum, 0);
}));
let max_column_width = max_column_em * em_size;
let column_width = Math.max(Math.min(max_child_width, max_column_width), min_column_em * em_size);
$column.width((Math.ceil(column_width / em_size) + 2) + 'em');
});
if ($related_tags.prop('scrollWidth') > ($related_tags.outerWidth() + (2 * em_size))) {
$('#irt-edit-scroll-wrapper').width($related_tags.outerWidth());
$('#irt-edit-scroll-bar').width($related_tags.prop('scrollWidth') - em_size);
$('#irt-edit-scroll-wrapper').show();
$related_tags.addClass('scrollable');
} else {
$('#irt-edit-scroll-wrapper').hide();
$related_tags.removeClass('scrollable');
}
}
function QueueRelatedTagColumnWidths() {
if (Number.isInteger(QueueRelatedTagColumnWidths.timer)) {
clearTimeout(QueueRelatedTagColumnWidths.timer);
}
QueueRelatedTagColumnWidths.timer = setTimeout(() => {
InitializeRelatedTagColumnWidths();
QueueRelatedTagColumnWidths.timer = null;
}, 100);
}
//Main execution functions
function SetupInitializations() {
const printer = JSPLib.debug.getFunctionPrint('SetupInitializations');
JSPLib.utility.recheckInterval({
check: () => (($('#related-tags-container .ai-tags-related-tags-column .tag-list').html() || "").trim() !== ""),
exec: () => {
printer.debuglog("Related tags found... initializing.");
InitializeRelatedTagsSection();
},
fail: () => {
printer.debuglog("Related tags not found... setting up event listener.");
$(document)
.on('danbooru:open-post-edit-tab.irt danbooru:open-post-edit-dialog.irt', () => {
printer.debuglog("Event listener triggered... initializing.");
InitializeRelatedTagsSection();
$(document).off('danbooru:open-post-edit-tab.irt danbooru:open-post-edit-dialog.irt');
});
},
interval: 250,
duration: JSPLib.utility.one_second * 10,
});
$(document).on('danbooru:close-post-edit-dialog.irt', QueueRelatedTagColumnWidths);
}
function CleanupTasks() {
JSPLib.storage.pruneProgramCache(PROGRAM_SHORTCUT, PROGRAM_DATA_REGEX, PRUNE_EXPIRES);
}
//Menu functions
function OptionCacheDataKey(data_type, data_value) {
if (data_type === 'related_tag') {
IRT.tag_category = $('#irt-control-tag-category').val();
IRT.query_order = $('#irt-control-query-order').val();
let modifier = GetRelatedKeyModifer(IRT.related_category, IRT.query_order);
return `${modifier}-${data_value}`;
}
if (data_type === 'wiki_page') {
return `wpt-${data_value}`;
}
if (data_type === 'tag_overlap') {
return `tagov-${data_value}`;
}
}
function DataTypeChange() {
let data_type = $('#irt-control-data-type').val();
let action = (data_type === 'related_tag' ? 'show' : 'hide');
$('.irt-options[data-setting=tag_category]')[action]();
$('.irt-options[data-setting=query_order]')[action]();
}
function InitializeMenuAutocomplete() {
const printer = JSPLib.debug.getFunctionPrint('InitializeMenuAutocomplete');
JSPLib.load.scriptWaitExecute(IRT, 'IAC', {
available: () => {
$('#irt-control-tag-name, #irt-checklist-frequent-tags').data('tag-query');
IRT.IAC.InitializeTagQueryAutocompleteIndexed('#irt-control-tag-name, #irt-checklist-frequent-tags textarea', null);
printer.debuglogLevel('Initialized IAC autocomplete on menu inputs.', JSPLib.debug.DEBUG);
},
fallback: () => {
JSPLib.danboru.initializeAutocomplete('#irt-control-tag-name, #irt-checklist-frequent-tags textarea', 'tag-query', 'tag_query');
printer.debuglogLevel('Initialized Danbooru autocomplete on menu inputs.', JSPLib.debug.DEBUG);
},
});
}
function InitializeProgramValues() {
const printer = JSPLib.debug.getFunctionPrint('InitializeProgramValues');
if (!JSPLib.storage.use_indexed_db) {
printer.debugwarn("No Indexed DB! Exiting...");
return false;
}
JSPLib.load.setProgramGetter(IRT, 'IAC', 'IndexedAutocomplete', 29.25);
return true;
}
function RenderSettingsMenu() {
$('#indexed-related-tags').append(JSPLib.menu.renderMenuFramework(MENU_CONFIG));
$('#irt-general-settings').append(JSPLib.menu.renderDomainSelectors());
$('#irt-related-tag-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", RELATED_TAG_SETTINGS_DETAILS));
$('#irt-related-tag-settings').append(JSPLib.menu.renderInputSelectors('related_query_categories', 'checkbox'));
$('#irt-related-tag-settings').append(JSPLib.menu.renderCheckbox('related_query_order_enabled'));
$('#irt-related-tag-settings').append(JSPLib.menu.renderInputSelectors('related_query_order_default', 'radio'));
$('#irt-related-tag-settings').append(JSPLib.menu.renderTextinput('related_results_limit', 5));
$('#irt-related-tag-settings').append(JSPLib.menu.renderCheckbox('expandable_related_section_enabled'));
$('#irt-tag-statistic-settings').append(JSPLib.menu.renderCheckbox('related_statistics_enabled'));
$('#irt-tag-statistic-settings').append(JSPLib.menu.renderTextinput('random_post_batches', 5));
$('#irt-tag-statistic-settings').append(JSPLib.menu.renderTextinput('random_posts_per_batch', 5));
$('#irt-checklist-settings').append(JSPLib.menu.renderCheckbox('checklist_tags_enabled'));
$('#irt-checklist-settings').append(JSPLib.menu.renderCheckbox('checklist_query_only_enabled'));
$('#irt-wiki-page-settings').append(JSPLib.menu.renderCheckbox('wiki_page_tags_enabled'));
$('#irt-wiki-page-settings').append(JSPLib.menu.renderCheckbox('other_wikis_enabled'));
$('#irt-wiki-page-settings').append(JSPLib.menu.renderCheckbox('unique_wiki_tags_enabled'));
$('#irt-wiki-page-settings').append(JSPLib.menu.renderCheckbox('wiki_page_query_only_enabled'));
$('#irt-wiki-page-settings').append(JSPLib.menu.renderCheckbox('query_unknown_tags_enabled'));
$('#irt-network-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", NETWORK_SETTINGS_DETAILS));
$('#irt-network-settings').append(JSPLib.menu.renderTextinput('recheck_data_interval', 5));
$('#irt-network-settings').append(JSPLib.menu.renderCheckbox('network_only_mode'));
$('#irt-checklist-controls').append(JSPLib.menu.renderCheckbox('import_export', true));
$('#irt-checklist-controls').append(JSPLib.menu.renderTextinput('tag_name', 50, true));
$('#irt-checklist-controls').append(CHECKLIST_TEXTAREA);
$('#irt-controls').append(JSPLib.menu.renderCacheControls());
$('#irt-cache-controls-message').append(JSPLib.menu.renderExpandable("Cache Data details", CACHE_DATA_DETAILS));
$('#irt-cache-controls').append(JSPLib.menu.renderLinkclick('cache_info', true));
$('#irt-cache-controls').append(JSPLib.menu.renderCacheInfoTable());
$('#irt-cache-controls').append(JSPLib.menu.renderLinkclick('purge_cache', true));
$('#irt-controls').append(JSPLib.menu.renderCacheEditor(true));
$('#irt-cache-editor-message').append(JSPLib.menu.renderExpandable("Program Data details", PROGRAM_DATA_DETAILS));
$('#irt-cache-editor-controls').append(JSPLib.menu.renderKeyselect('data_source', true));
$('#irt-cache-editor-controls').append(JSPLib.menu.renderDataSourceSections());
$('#irt-section-indexed-db').append(JSPLib.menu.renderKeyselect('data_type', true));
$('#irt-section-indexed-db').append(JSPLib.menu.renderKeyselect('tag_category', true));
$('#irt-section-indexed-db').append(JSPLib.menu.renderKeyselect('query_order', true));
$('#irt-section-local-storage').append(JSPLib.menu.renderCheckbox('raw_data', true));
$('#irt-cache-editor-controls').append(JSPLib.menu.renderTextinput('data_name', 20, true));
JSPLib.menu.engageUI(true, true);
JSPLib.menu.saveUserSettingsClick();
JSPLib.menu.resetUserSettingsClick();
$('#irt-tag-name-view').on(JSPLib.program_click, ViewChecklistTag);
$('#irt-tag-name-save').on(JSPLib.program_click, SaveChecklistTag);
$('#irt-tag-name-populate').on(JSPLib.program_click, PopulateChecklistTag);
$('#irt-tag-name-list').on(JSPLib.program_click, ListChecklistTags);
JSPLib.menu.cacheInfoClick();
JSPLib.menu.purgeCacheClick();
JSPLib.menu.expandableClick();
JSPLib.menu.dataSourceChange();
$('#irt-control-data-type').on('change.irt', DataTypeChange);
JSPLib.menu.rawDataChange();
JSPLib.menu.getCacheClick(ValidateProgramData);
JSPLib.menu.saveCacheClick(ValidateProgramData, ValidateEntry);
JSPLib.menu.deleteCacheClick();
JSPLib.menu.listCacheClick();
JSPLib.menu.refreshCacheClick();
JSPLib.menu.cacheAutocomplete();
InitializeMenuAutocomplete();
}
//Main program
function Main() {
const preload = {
run_on_settings: true,
initialize_func: InitializeProgramValues,
render_menu_func: RenderSettingsMenu,
program_css: PROGRAM_CSS,
light_css: LIGHT_MODE_CSS,
dark_css: DARK_MODE_CSS,
menu_css: MENU_CSS,
};
if (!JSPLib.menu.preloadScript(IRT, preload)) return;
SetupInitializations();
JSPLib.statistics.addPageStatistics(PROGRAM_NAME);
JSPLib.load.noncriticalTasks(CleanupTasks);
}
/****Initialization****/
//Variables for JSPLib
JSPLib.program_name = PROGRAM_NAME;
JSPLib.program_shortcut = PROGRAM_SHORTCUT;
JSPLib.program_data = IRT;
//Variables for debug.js
JSPLib.debug.mode = false;
JSPLib.debug.level = JSPLib.debug.INFO;
//Variables for menu.js
JSPLib.menu.program_data_regex = PROGRAM_DATA_REGEX;
JSPLib.menu.program_data_key = OptionCacheDataKey;
JSPLib.menu.settings_config = SETTINGS_CONFIG;
JSPLib.menu.control_config = CONTROL_CONFIG;
//Variables for storage.js
JSPLib.storage.indexedDBValidator = ValidateEntry;
//Export JSPLib
JSPLib.load.exportData();
/****Execution start****/
JSPLib.load.programInitialize(Main, {required_variables: PROGRAM_LOAD_REQUIRED_VARIABLES, required_selectors: PROGRAM_LOAD_REQUIRED_SELECTORS});