// ==UserScript==
// @name IndexedRelatedTags
// @namespace https://github.com/BrokenEagle/JavaScripts
// @version 3.1
// @description Uses Indexed DB for autocomplete, plus caching of other data.
// @source https://danbooru.donmai.us/users/23799
// @author BrokenEagle
// @match *://*.donmai.us/*
// @exclude /^https?://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/
// @grant none
// @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://cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/module.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/debug.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/utility.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/validate.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/storage.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/notice.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/concurrency.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/statistics.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/network.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/danbooru.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20220515/lib/load.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20240223-menu/lib/menu.js
// ==/UserScript==
/* global JSPLib $ Danbooru validate */
/****Global variables****/
//Library constants
JSPLib.validate.tag_categories = [0, 1, 3, 4, 5];
validate.validators.tagentryarray = function(value, options) {
if (options !== false) {
if (!validate.isArray(value)) {
return "is not an array";
}
let categories = (JSPLib.validate.checkOptions(options, 'categories') ? options.categories : JSPLib.validate.tag_categories);
for (let i = 0;i < value.length;i++) {
if (value[i].length !== 2) {
return "must have 2 entries in tag entry [" + i.toString() + "]";
}
if (!validate.isString(value[i][0])) {
return "must be a string [" + i.toString() + "][0]";
}
if (categories.indexOf(value[i][1]) < 0) {
return "must be a valid tag category [" + i.toString() + "][1]";
}
}
}
};
JSPLib.validate.tagentryarray_constraints = function(categories) {
let option = (Array.isArray(categories) ? {categories} : true);
return {
presence: true,
tagentryarray: option
};
};
const LIBRARY_MENU_CSS = `
#userscript-settings-menu .jsplib-settings-buttons input {
color: white;
}
#page #userscript-settings-menu .jsplib-settings-buttons .jsplib-commit:hover {
background-color: var(--green-5);
}
#page #userscript-settings-menu .jsplib-settings-buttons .jsplib-resetall:hover {
background-color: var(--red-5);
}
#userscript-settings-menu .jsplib-settings-buttons .jsplib-commit:hover,
#userscript-settings-menu .jsplib-settings-buttons .jsplib-resetall:hover {
filter: brightness(1.25);
}`;
//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_CLICK = 'click.irt';
const PROGRAM_KEYDOWN = 'keydown.irt';
const PROGRAM_MOUSEENTER = 'mouseenter.irt';
const PROGRAM_MOUSELEAVE = 'mouseleave.irt';
const PROGRAM_SCROLL = 'scroll.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 = {};
const FUNC = {};
//For factory reset
const LOCALSTORAGE_KEYS = [];
const PROGRAM_RESET_KEYS = {};
//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.validate.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.validate.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.validate.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.validate.isBoolean,
hint: "Include wiki page tags when using one of the related tags buttons."
},
wiki_page_query_only_enabled: {
reset: false,
validate: JSPLib.validate.isBoolean,
hint: "Include a button to query only wiki page tags."
},
checklist_tags_enabled: {
reset: false,
validate: JSPLib.validate.isBoolean,
hint: "Include checklist tags when using one of the related tags buttons."
},
checklist_query_only_enabled: {
reset: false,
validate: JSPLib.validate.isBoolean,
hint: "Include a button to add only checklist tags."
},
query_unknown_tags_enabled: {
reset: false,
validate: JSPLib.validate.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.validate.isBoolean,
hint: "Include list_of_* wikis when including wiki page tags."
},
unique_wiki_tags_enabled: {
reset: true,
validate: JSPLib.validate.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.validate.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."
}],
};
// Default values
const DEFAULT_VALUES = PROGRAM_RESET_KEYS;
//Pre-CSS/HTML constants
const DEPRECATED_TAG_CATEGORY = 200;
const NONEXISTENT_TAG_CATEGORY = 300;
const BUR_TAG_CATEGORY = 400;
const METATAG_TAG_CATEGORY = 500;
//CSS Constants
const PROGRAM_CSS = `
.irt-line-entry {
display: flex;
width: 100%;
white-space: nowrap;
}
.irt-line-entry a {
white-space: normal;
}
.irt-query > span:first-of-type,
.irt-pool > span:first-of-type,
.irt-favgroup > span:first-of-type,
.irt-artist > span:first-of-type,
.irt-forum-topic > span:first-of-type {
flex-basis: 90%;
}
.irt-query > span:last-of-type,
.irt-pool > span:last-of-type,
.irt-favgroup > span:last-of-type,
.irt-artist > span:last-of-type,
.irt-forum-topic > span:last-of-type {
flex-basis: 10%;
text-align: right;
}
.irt-wiki-page > span:first-of-type {
flex-basis: 85%;
}
.irt-wiki-page > span:last-of-type {
flex-basis: 15%;
text-align: right;
}
.irt-user > span,
irt-search > span {
flex-basis: 100%;
}
.irt-user-choice .autocomplete-item {
box-shadow: 0px 2px 0px #000;
padding-bottom: 1px;
line-height: 150%;
}
.irt-tag-alias a {
font-style: italic;
}
.irt-tag-highlight {
margin-top: -5px;
margin-bottom: 5px;
}
.irt-tag-highlight > div:before {
content: "●";
padding-right: 4px;
font-weight: bold;
font-size: 150%;
}
.irt-tag-bur > div:before {
color: #000;
}
.irt-tag-exact > div:before {
color: #DDD;
}
.irt-tag-word > div:before {
color: #888;
}
.irt-tag-abbreviation > div:before {
color: hotpink;
}
.irt-tag-alias > div:before {
color: gold;
}
.irt-tag-autocorrect > div:before {
color: cyan;
}
.irt-tag-other-name > div:before {
color: orange;
}
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:link,
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:visited {
color: skyblue;
}
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:link,
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:visited {
color: darkgrey;
}
.tag-type-${NONEXISTENT_TAG_CATEGORY} a.search-tag:hover,
.tag-type-${DEPRECATED_TAG_CATEGORY} a.search-tag:hover {
filter: brightness(1.25);
}
.irt-tag-bur > div:before,
.irt-tag-highlight .tag-type-${BUR_TAG_CATEGORY}:link,
.irt-tag-highlight .tag-type-${BUR_TAG_CATEGORY}:visited,
.irt-tag-highlight .tag-type-${BUR_TAG_CATEGORY}:hover {
color: #888;
}
.irt-highlight-match {
font-weight: bold;
}
.irt-related-tags .tag-column li.selected {
font-weight: bold;
}
.irt-related-tags .tag-column li:before {
content: "*";
font-family: monospace;
font-weight: bold;
visibility: hidden;
padding-right: 0.2em;
}
.irt-related-tags .tag-column li.selected:before {
visibility: visible;
}
div#edit-dialog div#irt-related-tags-container {
max-height: 400px;
overflow-y: auto;
}
/** DARK/LIGHT Color Setup **/
body[data-current-user-theme=light] .irt-already-used {
background-color: #FFFFAA;
}
body[data-current-user-theme=light] .irt-tag-metatag > div:before,
body[data-current-user-theme=light] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=light] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=light] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #000;
}
body[data-current-user-theme=light] .irt-highlight-match {
filter: brightness(0.75);
}
body[data-current-user-theme=dark] .irt-already-used {
background-color: #666622;
}
body[data-current-user-theme=dark] .irt-tag-metatag > div:before,
body[data-current-user-theme=dark] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=dark] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=dark] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #FFF;
}
body[data-current-user-theme=dark] .irt-highlight-match {
filter: brightness(1.25);
}
@media (prefers-color-scheme: light) {
body[data-current-user-theme=auto] .irt-already-used {
background-color: #FFFFAA;
}
body[data-current-user-theme=auto] .irt-tag-metatag > div:before,
body[data-current-user-theme=auto] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=auto] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=auto] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #000;
}
body[data-current-user-theme=auto] .irt-highlight-match {
filter: brightness(0.75);
}
}
@media (prefers-color-scheme: dark) {
body[data-current-user-theme=auto] .irt-already-used {
background-color: #666622;
}
body[data-current-user-theme=auto] .irt-tag-metatag > div:before,
body[data-current-user-theme=auto] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:link,
body[data-current-user-theme=auto] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:visited,
body[data-current-user-theme=auto] .irt-tag-highlight .tag-type-${METATAG_TAG_CATEGORY}:hover {
color: #FFF;
}
body[data-current-user-theme=auto] .irt-highlight-match {
filter: brightness(1.25);
}
}
`;
const RELATED_QUERY_CONTROL_CSS = `
.irt-related-button {
margin: 0 2px;
}
.irt-related-button[disabled] {
cursor: default;
}
#irt-wiki-page-query {
color: white;
font-weight: bold;
background-color: green;
border-color: darkgreen;
margin-right: 0.5em;
}
#irt-checklist-query {
color: white;
font-weight: bold;
background-color: orange;
border-color: darkorange;
margin-right: 0.5em;
}
#irt-related-query-type label {
color: black;
background-color: lightgrey;
margin-right: 0.5em;
font-weight: bold;
}
#irt-related-query-type .ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
#irt-related-query-type .ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
background-image: none;
width: 8px;
height: 8px;
border-width: 4px;
border-style: solid;
}
#irt-related-query-type .ui-state-active .ui-icon-background {
border: black;
background-color: white;
}
#irt-related-query-type .ui-visual-focus,
#irt-related-query-type .ui-state-active,
#irt-related-query-type .ui-widget-content .ui-state-active,
#irt-related-query-type .ui-button.ui-state-active:hover,
#irt-related-query-type .ui-button.ui-state-active:focus,
#irt-related-query-type .ui-button:focus,
#irt-related-query-type .ui-button:active {
border: 1px solid white;
background: lightgrey;
outline: none;
box-shadow: none;
}`;
const EXPANDABLE_RELATED_SECTION_CSS = `
#irt-edit-scroll-wrapper {
height: 20px;
overflow-x: scroll;
overflow-y: hidden;
display: none;
}
#irt-edit-scroll-bar {
height: 20px;
}
.irt-tag-statistic {
color: hotpink;
}
div#irt-related-tags-container div.irt-related-tags {
overflow-x: hidden;
flex-wrap: nowrap;
max-width: calc(100% - 2em);
display: inline-flex;
}
div#irt-related-tags-container div.irt-related-tags.scrollable {
overflow-x: scroll;
}
div#irt-related-tags-container div.irt-related-tags div.tag-column {
width: 18em;
max-width: unset;
margin-right: 1em;
}
div#irt-related-tags-container div.irt-related-tags div.tag-column.irt-general-related-tags-column.irt-is-empty-false {
width: 18em;
}
div#irt-related-tags-container div.irt-related-tags div.tag-column.irt-is-empty-true {
display: none;
}`;
const SETTINGS_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 = `
All timestamps are in milliseconds since the epoch (Epoch converter).