// ==UserScript==
// @name RecentTagsCalc
// @namespace https://github.com/BrokenEagle/JavaScripts
// @version 7.28
// @description Use different mechanism to calculate RecentTags.
// @source https://danbooru.donmai.us/users/23799
// @author BrokenEagle
// @match *://*.donmai.us/uploads/*
// @match *://*.donmai.us/posts/*
// @match *://*.donmai.us/settings
// @exclude /^https?://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/
// @grant none
// @run-at document-end
// @downloadURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/RecentTagsCalc.user.js
// @updateURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/RecentTagsCalc.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/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 */
/****Global variables****/
//Library constants
////NONE
//Exterior script variables
const DANBOORU_TOPIC_ID = '15851';
//Variables for load.js
const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery','window.Danbooru','Danbooru.CurrentUser'];
const PROGRAM_LOAD_OPTIONAL_SELECTORS = ['#c-uploads #a-show', '#c-upload-media-assets #a-show', '#c-posts #a-show', '#c-users #a-edit'];
//Program name constants
const PROGRAM_SHORTCUT = 'rtc';
const PROGRAM_CLICK = 'click.rtc';
const PROGRAM_NAME = 'RecentTagsCalc';
//Program data constants
const PROGRAM_DATA_REGEX = /^(ta|tag)-/; //Regex that matches the prefix of all program cache data
const PROGRAM_DATA_KEY = {
tag_data: 'tag',
tag_alias: 'ta'
};
//Main program variable
const RTC = {};
//For factory reset
const localstorage_keys = [
'rtc-pinned-tags',
'rtc-recent-tags',
'rtc-other-recent',
'rtc-frequent-tags',
'rtc-frequent-tags-expires',
'rtc-was-upload',
];
const PROGRAM_RESET_KEYS = {
pinned_tags: [],
recent_tags: [],
other_recent: [],
frequent_tags: [],
tag_data: {},
};
const order_types = ['alphabetic','form_order','post_count','category','tag_usage'];
const category_orders = ['general','artist','copyright','character','meta','alias','metatag'];
const list_types = ['queue','single','multiple'];
const disabled_order_types = ['tag_usage'];
const SETTINGS_CONFIG = {
uploads_order: {
allitems: order_types,
reset: ['form_order'],
validate: (data) => JSPLib.menu.validateCheckboxRadio(data, 'radio', order_types),
hint: "Select the type of order to be applied on recent tags from an upload."
},
post_edits_order: {
allitems: order_types,
reset: ['alphabetic'],
validate: (data) => JSPLib.menu.validateCheckboxRadio(data, 'radio', order_types),
hint: "Select the type of order to be applied on recent tags from a post edit."
},
metatags_first: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Sets the post count high for metatags. Only effective with the Post Count order type."
},
aliases_first: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Sets the post count high for aliases. Only effective with the Post Count order type."
},
category_order: {
allitems: category_orders,
reset: category_orders,
sortvalue: true,
validate: (data) => JSPLib.utility.arrayEquals(data, category_orders),
hint: "Drag and drop the categories to determine the group order for the Category order type."
},
list_type: {
allitems: list_types,
reset: ['queue'],
validate: (data) => JSPLib.menu.validateCheckboxRadio(data, 'radio', list_types),
hint: "Select how to store tags after each upload/edit."
},
maximum_tags: {
reset: 25,
parse: parseInt,
validate: (data) => JSPLib.menu.validateNumber(data, true, 1),
hint: "The number of recent tags to store and show."
},
maximum_tag_groups: {
reset: 5,
parse: parseInt,
validate: (data) => JSPLib.menu.validateNumber(data, true, 1),
hint: "Number of recent tag groups to store and show. Only affects the Multiple list type."
},
include_metatags: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Does not filter out metatags."
},
include_unchanged_tags: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Does not filter out unchanged tags."
},
include_removed_tags: {
reset: false,
validate: JSPLib.validate.isBoolean,
hint: "Does not filter out removed tags."
},
include_deleted_tags: {
reset: false,
validate: JSPLib.validate.isBoolean,
hint: "Does not filter out unaliased tags with a post count of 0."
},
cache_frequent_tags: {
reset: true,
validate: JSPLib.validate.isBoolean,
hint: "Saves the user's favorite tags locally."
}
};
const all_source_types = ['indexed_db','local_storage'];
const all_data_types = ['tag_data', 'tag_alias', 'custom'];
const CONTROL_CONFIG = {
refresh_frequent_tags: {
value: "Click to refresh",
hint: "Gets the latest favorite tags from the user's profile.",
},
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: 'tag',
hint: "Select type of data. Use Custom for querying by keyname.",
},
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.",
},
};
const MENU_CONFIG = {
topic_id: DANBOORU_TOPIC_ID,
settings: [{
name: 'general',
},{
name: 'order',
},{
name: 'list',
},{
name: 'inclusion',
},{
name: 'frequent',
}],
controls: [{
name: 'frequent',
}],
};
const DEFAULT_VALUES = PROGRAM_RESET_KEYS;
//Misc tag categories
const alias_tag_category = 100;
const deleted_tag_category = 200;
const notfound_tag_category = 300;
const metatags_category = 400;
const category_name = {
0: "general",
1: "artist",
3: "copyright",
4: "character",
5: "meta",
[alias_tag_category]: "alias",
[metatags_category]: "metatag",
[deleted_tag_category]: "deleted"
};
//CSS Constants
let program_css = `
.rtc-user-related-tags-columns {
display: flex;
}
.tag-type-${metatags_category} a:link,
.tag-type-${metatags_category} a:visited {
color: darkgoldenrod;
font-weight: bold;
}
.tag-type-${metatags_category} a:hover {
color: goldenrod;
font-weight: bold;
}
.tag-type-${alias_tag_category} a:link,
.tag-type-${alias_tag_category} a:visited {
color: #0CC;
font-weight: bold;
}
.tag-type-${alias_tag_category} a:hover {
color: aqua;
font-weight: bold;
}
.tag-type-${deleted_tag_category} a:link,
.tag-type-${deleted_tag_category} a:visited {
color: black;
background-color: red;
font-weight: bold;
}
.tag-type-${deleted_tag_category} a:hover {
color: black;
background-color: white;
font-weight: bold;
}
.tag-type-${notfound_tag_category} a {
text-decoration: underline dotted grey;
}
.tag-column {
overflow: hidden;
}
.rtc-user-related-tags-columns .frequent-related-tags-column li:before {
content: "*";
font-family: monospace;
font-weight: bold;
visibility: hidden;
padding-right: 0.2em;
}
.rtc-user-related-tags-columns .frequent-related-tags-column li.selected:before {
visibility: visible;
}
.user-related-tags-columns .frequent-related-tags-column,
.user-related-tags-columns .recent-related-tags-column {
display: none;
}
`;
const MENU_CSS = `
#recent-tags-calc .rtc-sortlist li {
width: 150px;
}
#recent-tags-calc .jsplib-settings-grouping:not(#rtc-general-settings) .rtc-selectors label {
width: 120px;
}`;
//HTML Constants
const usertag_columns_html = `
`;
const ORDER_SETTINGS_DETAILS = `
- Order types: for Uploads Order and Post Edits Order
- Alphabetic: A to Z.
- Form order: The order of tags in the tag edit box.
- Post count: Highest to lowest.
- Metatags are rated higher than aliases.
- Only when both Metatags First and Aliases First are set.
- Category: Tag category.
- Tag usage: Ordered by recent tag usage.
`;
const LIST_SETTINGS_DETAILS = `
- List type:
- Queue: First in, first out.
- Single: Only the tags from the last upload/edit.
- Multiple: Each upload/edit gets its own list.
`;
const INCLUSION_SETTINGS_DETAILS = `
- Include removed tags:
- This includes both tags removed through deletion and through negative tags.
- When Form Order is being used, tag deletions get appended onto the new set of recent tags.
`;
const FREQUENT_SETTINGS_DETAILS = `
- Cache frequent tags:
- Makes for quicker loading of recent/frequent tags.
- Tags are automatically refreshed once a week.
`;
const CACHE_DATA_DETAILS = `
- Tag data (tag): Used to determine a tag's post count and category.
- Tag aliases (ta): Used to determine which tags are aliases or deleted.
`;
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.
- Recent tags
- recent-tags: The current list of recent tags.
- pinned-tags: The current list of pinned tags.
- Used when the Multiple type list is selected:
- other-recent: Groups of the most recent tags used, set with the type of post event.
- was-upload: Determines whether the current recent tags were from an upload or edit.
- process-semaphore-recent: Prevents two tabs from processing the same recent data at the same time.
- Frequent tags
- frequent-tags: List of all favorite tags from the user.
- frequent-tags-expires: When to next query the user's profile.
- process-semaphore-frequent: Prevents two tabs from processing the same frequency data at the same time.
`;
//Expirations
const prune_expires = JSPLib.utility.one_day;
const noncritical_recheck = JSPLib.utility.one_minute;
const tag_expires = JSPLib.utility.one_week;
const frequent_tags_expires = JSPLib.utility.one_week;
//Tag regexes
const negative_regex = /^-/;
const metatags_regex = /^(?:rating|-?parent|source|-?locked|-?pool|newpool|-?fav|child|-?favgroup|upvote|downvote):/i;
const striptype_regex = /^(-?)(?:general:|gen:|artist:|art:|copyright:|copy:|co:|character:|char:|ch:|meta:)?(.*)/i;
//For when new data has yet to be loaded by another tab
const default_tag_data = {
category: notfound_tag_category,
is_alias: false,
is_deleted: false,
postcount: 0
};
const deleted_tag_data = {
category: deleted_tag_category,
postcount: 0,
is_alias: false,
is_deleted: true
};
//Misc constants
const timer_poll_interval = 100;
const max_item_limit = 100;
const aliases_first_post_count = 1000000000;
const metatags_first_post_count = 2000000000;
const tag_fields = "id,name,category,post_count";
const user_fields = "favorite_tags";
//Validation values
const relation_constraints = {
entry: JSPLib.validate.arrayentry_constraints,
value: JSPLib.validate.basic_stringonly_validator
};
const tag_constraints = {
entry: JSPLib.validate.hashentry_constraints,
value: {
category: JSPLib.validate.inclusion_constraints([0,1,2,3,4,5,alias_tag_category,deleted_tag_category]),
postcount: JSPLib.validate.counting_constraints,
is_alias: JSPLib.validate.boolean_constraints,
is_deleted: JSPLib.validate.boolean_constraints
}
};
const other_recent_constraints = {
tags: JSPLib.validate.array_constraints,
was_upload: JSPLib.validate.boolean_constraints,
};
/****Functions****/
//Library functions
////NONE
//Validation functions
function ValidateEntry(key,entry) {
if (!JSPLib.validate.validateIsHash(key,entry)) {
return false;
}
if (key.match(/^tag-/)) {
return ValidateTagEntry(key,entry);
} else if (key.match(/^ta-/)) {
return ValidateRelationEntry(key,entry);
}
this.debug('log',"Bad key!");
return false;
}
function ValidateTagEntry(key,entry) {
if (!JSPLib.validate.validateHashEntries(key, entry, tag_constraints.entry)) {
return false;
}
if (!JSPLib.validate.validateHashEntries(key + '.value', entry.value, tag_constraints.value)) {
return false;
}
return true;
}
function ValidateRelationEntry(key,entry) {
if (!JSPLib.validate.validateHashEntries(key, entry, relation_constraints.entry)) {
return false;
}
return JSPLib.validate.validateArrayValues(key + '.value', entry.value, relation_constraints.value);
}
function ValidateProgramData(key,entry) {
var checkerror = [],maximum_validator;
switch (key) {
case 'rtc-user-settings':
checkerror = JSPLib.menu.validateUserSettings(entry,SETTINGS_CONFIG);
break;
case 'rtc-prune-expires':
case 'rtc-frequent-tags-expires':
case 'rtc-process-semaphore-recent':
case 'rtc-process-semaphore-frequent':
if (!Number.isInteger(entry)) {
checkerror = ["Value is not an integer."];
}
break;
case 'rtc-was-upload':
if (!JSPLib.validate.isBoolean(entry)) {
checkerror = ['Value is not a boolean.'];
}
break;
case 'rtc-recent-tags':
case 'rtc-pinned-tags':
case 'rtc-frequent-tags':
maximum_validator = (key === 'rtc-recent-tags' ? {maximum: RTC.user_settings.maximum_tags} : undefined);
if (!JSPLib.validate.validateIsArray(key,entry,maximum_validator)) {
return false;
}
return JSPLib.validate.validateArrayValues(key,entry,JSPLib.validate.basic_stringonly_validator);
case 'rtc-other-recent':
if (!JSPLib.validate.validateIsArray(key,entry,{maximum: RTC.user_settings.maximum_tag_groups})) {
return false;
}
for (let i = 0;i < entry.length; i++) {
let entry_key = `${key}[${i}]`;
if (!JSPLib.validate.validateIsHash(entry_key,entry[i])) {
return false;
}
if (!JSPLib.validate.validateHashEntries(entry_key, entry[i], other_recent_constraints)) {
return false;
}
if (!JSPLib.validate.validateArrayValues(entry_key+'.tags',entry[i].tags,JSPLib.validate.basic_stringonly_validator)) {
return false;
}
}
return true;
default:
checkerror = ["Not a valid program data key."];
}
if (checkerror.length) {
JSPLib.validate.outputValidateError(key,checkerror);
return false;
}
return true;
}
//Auxiliary functions
function GetTagList() {
return JSPLib.utility.filterEmpty(StripQuoteSourceMetatag($("#upload_tag_string,#post_tag_string").val()).split(/[\s\n]+/).map((tag) => tag.toLowerCase()));
}
function StripQuoteSourceMetatag(str) {
return str.replace(/source:"[^"]+"\s?/g,'');
}
function GetNegativetags(array) {
return JSPLib.utility.filterRegex(array,negative_regex,false).map((value) => value.substring(1));
}
function FilterMetatags(array) {
return JSPLib.utility.filterRegex(array,metatags_regex,true);
}
function NormalizeTags(array) {
return array.map((entry) => entry.replace(/^-/, ''));
}
function TransformTypetags(array) {
return array.map((value) => value.match(striptype_regex).splice(1).join(''));
}
function GetCurrentTags() {
let tag_list = GetTagList();
if (!RTC.user_settings.include_metatags) {
tag_list = JSPLib.utility.filterRegex(GetTagList(),metatags_regex,true);
}
return TransformTypetags(tag_list);
}
function GetTagCategory(tag) {
let tag_data = GetTagData(tag);
if (!tag_data) {
return 0;
}
return tag_data.category;
}
function GetTagData(tag) {
if (tag.match(metatags_regex)) {
let postcount = (RTC.user_settings.metatags_first ? metatags_first_post_count : 0);
return {postcount:postcount,category:metatags_category};
}
if (!(tag in RTC.tag_data) || RTC.tag_data[tag].category === notfound_tag_category) {
RTC.tag_data[tag] = JSPLib.storage.getIndexedSessionData('tag-'+tag,{value:default_tag_data}).value;
}
if (RTC.tag_data[tag].is_alias) {
RTC.tag_data[tag].category = alias_tag_category;
RTC.tag_data[tag].postcount = (RTC.user_settings.aliases_first ? aliases_first_post_count : 0);
} else if (RTC.tag_data[tag].is_deleted) {
RTC.tag_data[tag].category = deleted_tag_category;
}
return RTC.tag_data[tag];
}
function GetTagColumnList(name) {
if (name === "frequent") {
return RTC.frequent_tags;
} else if (name === "recent") {
let all_tags = RTC.recent_tags;
if (RTC.user_settings.list_type[0] === "multiple") {
RTC.other_recent.forEach((recent_entry)=>{
all_tags = JSPLib.utility.arrayUnion(all_tags,recent_entry.tags);
});
}
return all_tags;
}
return [];
}
function GetStartingTags() {
return Object.keys(sessionStorage).filter((key) => key.match(/^danbooru-storage-tag-/)).map((tag) => tag.replace(/^danbooru-storage-tag-/, ''));
}
//Display functions
async function DisplayRecentTags() {
await RTC.pageload_recentcheck;
let $tag_column = $(".rtc-user-related-tags-columns .recent-related-tags-column");
let html = RenderTaglist(RTC.recent_tags,"Recent",RTC.pinned_tags);
if (RTC.user_settings.list_type[0] === "multiple") {
let upload = 1, edit = 1;
let shown_tags = JSPLib.utility.arrayUnion(RTC.recent_tags,RTC.pinned_tags);
RTC.other_recent.forEach((recent_entry)=>{
let title = (recent_entry.was_upload ? `Upload ${upload++}` : `Edit ${edit++}`);
let display_tags = JSPLib.utility.arrayDifference(recent_entry.tags,shown_tags);
if (display_tags.length) {
html += RenderTaglist(display_tags,title,[]);
}
shown_tags = JSPLib.utility.arrayUnion(shown_tags,display_tags);
});
}
$tag_column.html(html);
$tag_column.removeClass("is-empty-true").addClass("is-empty-false");
Danbooru.RelatedTag.update_selected();
$(".recent-related-tags-column .ui-icon").on(PROGRAM_CLICK,PinnedTagsToggle);
}
async function DisplayFrequentTags() {
await RTC.pageload_frequentcheck;
let $tag_column = $(".rtc-user-related-tags-columns .frequent-related-tags-column");
let html = RenderTaglist(RTC.frequent_tags,"Frequent");
$tag_column.html(html);
$tag_column.removeClass("is-empty-true").addClass("is-empty-false");
Danbooru.RelatedTag.update_selected();
}
function RecheckAndDisplay(name) {
JSPLib.storage.batchStorageCheck(FilterMetatags(GetTagColumnList(name)),ValidateEntry,tag_expires,'tag')
.then(()=>{
switch(name) {
case "recent":
DisplayRecentTags();
break;
case "frequent":
DisplayFrequentTags();
break;
default:
//do nothing
}
});
}
function RecheckDisplaySemaphore(name) {
JSPLib.utility.recheckTimer({
check: ()=>{return JSPLib.concurrency.checkSemaphore(PROGRAM_SHORTCUT, name);},
exec: ()=>{
this.debug('log',"Callback:",name);
RecheckAndDisplay(name);
}
},timer_poll_interval);
}
function RenderTaglines(taglist,addon) {
return taglist.map((tag)=>{
let category = GetTagCategory(tag);
let escaped_tag = JSPLib.utility.HTMLEscape(tag);
let search_link = JSPLib.danbooru.postSearchLink(tag,tag.replace(/_/g,' '),`class="search-tag" data-tag-name="${escaped_tag}"`);
return ` ${addon}${search_link}\n`;
}).join('');
}
function RenderTaglist(taglist,columnname,pinned_tags) {
let html = "";
if (pinned_tags && pinned_tags.length) {
html += RenderTaglines(pinned_tags,` `);
taglist = JSPLib.utility.arrayDifference(taglist,pinned_tags);
}
let pin_html = (pinned_tags ? ` ` : '');
html += RenderTaglines(taglist,pin_html);
return `
${columnname}
`;
}
//Event handlers
function ReloadFrequentTags(event) {
QueryFrequentTags().then(()=>{
JSPLib.notice.notice(`${PROGRAM_NAME}: Frequent tags reloaded!`);
});
event.preventDefault();
}
function PinnedTagsToggle(event) {
$(event.target).toggleClass("ui-icon-radio-off ui-icon-pin-s");
let tag_name = $(".search-tag",event.target.parentElement).text().replace(/\s/g,'_');
RTC.pinned_tags = JSPLib.utility.arraySymmetricDifference(RTC.pinned_tags,[tag_name]);
JSPLib.storage.setStorageData('rtc-pinned-tags',RTC.pinned_tags,localStorage);
RTC.channel.postMessage({type: "reload_recent", recent_tags: RTC.recent_tags, pinned_tags: RTC.pinned_tags, other_recent: RTC.other_recent, updated_pin_tag: tag_name});
}
function CaptureTagSubmission() {
RTC.postedittags = GetCurrentTags();
RTC.new_recent_tags = NormalizeTags(RTC.postedittags);
RTC.positivetags = JSPLib.utility.filterRegex(RTC.postedittags,negative_regex,true);
RTC.negativetags = GetNegativetags(RTC.postedittags);
RTC.userremovetags = JSPLib.utility.arrayDifference(RTC.preedittags,RTC.positivetags);
RTC.removedtags = JSPLib.utility.arrayUnion(RTC.userremovetags,RTC.negativetags);
RTC.unchangedtags = JSPLib.utility.arrayDifference(JSPLib.utility.arrayIntersection(RTC.preedittags,RTC.positivetags),RTC.negativetags);
if (!RTC.user_settings.include_unchanged_tags) {
RTC.new_recent_tags = JSPLib.utility.arrayDifference(RTC.new_recent_tags,RTC.unchangedtags);
}
if (!RTC.user_settings.include_removed_tags) {
RTC.new_recent_tags = JSPLib.utility.arrayDifference(RTC.new_recent_tags,RTC.removedtags);
} else {
RTC.new_recent_tags = RTC.new_recent_tags.concat(RTC.userremovetags);
}
switch(RTC.tag_order) {
case "alphabetic":
RTC.new_recent_tags.sort();
break;
case "post_count":
case "category":
JSPLib.storage.setStorageData('rtc-new-recent-tags',RTC.new_recent_tags,localStorage);
RTC.new_recent_tags = RTC.recent_tags;
break;
case "form_order":
default:
//Do nothing
}
this.debug('log',"New recent tags:",RTC.new_recent_tags);
AddRecentTags(RTC.new_recent_tags);
}
//Main helper functions
async function CheckMissingTags(tag_list,list_name="") {
this.debug('log',"Checking tag list:", tag_list);
let network_tags = [];
let [found_tags,missing_tags] = await JSPLib.storage.batchStorageCheck(tag_list,ValidateEntry,tag_expires,'tag');
if (missing_tags.length) {
this.debug('log',"Missing tags:",missing_tags);
network_tags = await QueryMissingTags(missing_tags);
} else {
this.debug('log',`No missing tags in DB [${list_name}]!`);
}
let unavailable_tags = JSPLib.utility.arrayDifference(found_tags, RTC.starting_tags);
this.debug('log',"Unavailable tags:", unavailable_tags);
if (network_tags.length || unavailable_tags.length) {
let reload_tags = JSPLib.utility.arrayUnion(network_tags, unavailable_tags);
reload_tags.forEach((tag)=>{
let category = GetTagCategory(tag);
let escaped_tag = JSPLib.utility.HTMLEscape(tag);
$(`li.tag-type-${notfound_tag_category} a[href$="${escaped_tag}"]`).closest('li').removeClass().addClass(`tag-type-${category}`);
});
}
return [unavailable_tags,missing_tags];
}
async function QueryMissingTags(missing_taglist) {
let promise_array = [];
let tag_query = missing_taglist.join(' ');
let url_addons = {search: {name_space: tag_query, hide_empty: false}, only: tag_fields};
let queried_tags = await JSPLib.danbooru.getAllItems('tags', max_item_limit, {url_addons});
queried_tags.forEach((tagentry)=>{
let entryname = 'tag-' + tagentry.name;
let value = {
category: tagentry.category,
postcount: tagentry.post_count,
is_alias: tagentry.post_count === 0,
is_deleted: false
};
RTC.tag_data[tagentry.name] = value;
promise_array.push(JSPLib.storage.saveData(entryname, {value: value, expires: Date.now() + tag_expires}));
});
let network_tags = JSPLib.utility.getObjectAttributes(queried_tags, 'name');
let unfound_tags = JSPLib.utility.arrayDifference(missing_taglist, network_tags);
this.debug('log',"Network tags:", network_tags);
this.debug('log',"Unfound tags:", unfound_tags);
unfound_tags.forEach((tag)=>{
let entryname = 'tag-' + tag;
RTC.tag_data[tag] = deleted_tag_data;
promise_array.push(JSPLib.storage.saveData(entryname, {value: deleted_tag_data, expires: Date.now() + tag_expires}));
});
await Promise.all(promise_array);
RTC.channel.postMessage({type: "update_category", network_tags: network_tags});
return network_tags;
}
function FilterDeletedTags() {
JSPLib.debug.debugExecute(()=>{
RTC.deleted_saved_recent_tags = RTC.saved_recent_tags.filter((tag) => (GetTagCategory(tag) === deleted_tag_category));
RTC.deleted_recent_tags = RTC.recent_tags.filter((tag) => (GetTagCategory(tag) === deleted_tag_category));
if (RTC.deleted_saved_recent_tags.length || RTC.deleted_recent_tags.length) {
this.debug('log',"Deleting tags:",RTC.deleted_saved_recent_tags,RTC.deleted_recent_tags);
}
});
RTC.saved_recent_tags = RTC.saved_recent_tags.filter((tag) => (GetTagCategory(tag) !== deleted_tag_category));
RTC.recent_tags = RTC.recent_tags.filter((tag) => (GetTagCategory(tag) !== deleted_tag_category));
}
function SortTagData(tag_list,type) {
this.debug('log',"Pre-sort:",tag_list);
if (type === "post_count") {
tag_list.sort((a,b)=>{
let a_data = GetTagData(a);
let b_data = GetTagData(b);
return b_data.postcount - a_data.postcount;
});
} else if (type === "category") {
let category_order = RTC.user_settings.category_order.concat(['deleted']);
tag_list.sort((a,b)=>{
let a_data = GetTagCategory(a);
let b_data = GetTagCategory(b);
return category_order.indexOf(category_name[a_data]) - category_order.indexOf(category_name[b_data]);
});
}
this.debug('log',"Post-sort:",tag_list);
}
//Main execution functions
////Recent tags
async function CheckAllRecentTags() {
if (!JSPLib.concurrency.reserveSemaphore(PROGRAM_SHORTCUT, 'recent')) {
RecheckDisplaySemaphore("recent");
return;
}
let original_recent_tags = JSPLib.utility.dataCopy(RTC.recent_tags);
RTC.saved_recent_tags = [];
let tag_list = RTC.recent_tags.concat(RTC.pinned_tags);
if (RTC.tag_order === "post_count" || RTC.tag_order === "category") {
RTC.saved_recent_tags = JSPLib.storage.checkStorageData('rtc-new-recent-tags',ValidateProgramData,localStorage,[]);
tag_list = JSPLib.utility.arrayUnion(tag_list,RTC.saved_recent_tags);
}
if (RTC.user_settings.list_type[0] === "multiple") {
RTC.other_recent.forEach((recent_entry)=>{
tag_list = JSPLib.utility.arrayUnion(tag_list,recent_entry.tags);
});
}
RTC.missing_recent_tags = await CheckMissingTags(FilterMetatags(tag_list), "Recent");
if (!RTC.user_settings.include_deleted_tags) {
FilterDeletedTags();
}
if ((RTC.tag_order === "post_count" || RTC.tag_order === "category") && RTC.saved_recent_tags.length) {
SortTagData(RTC.saved_recent_tags,RTC.tag_order);
}
JSPLib.storage.removeStorageData('rtc-new-recent-tags', localStorage);
if (JSPLib.utility.arraySymmetricDifference(original_recent_tags,RTC.recent_tags).length || RTC.saved_recent_tags.length) {
AddRecentTags(RTC.saved_recent_tags);
}
JSPLib.concurrency.freeSemaphore(PROGRAM_SHORTCUT, 'recent');
}
function AddRecentTags(newtags) {
switch (RTC.user_settings.list_type[0]) {
case "multiple":
RTC.was_upload = JSPLib.storage.checkStorageData('rtc-was-upload',ValidateProgramData,localStorage,false);
if (newtags.length && RTC.recent_tags.length) {
RTC.other_recent.unshift({
was_upload: RTC.was_upload,
tags: RTC.recent_tags
});
RTC.other_recent = RTC.other_recent.slice(0,RTC.user_settings.maximum_tag_groups);
JSPLib.storage.setStorageData('rtc-other-recent',RTC.other_recent,localStorage);
}
JSPLib.storage.setStorageData('rtc-was-upload',RTC.is_upload,localStorage);
//falls through
case "single":
if (newtags.length) {
RTC.recent_tags = newtags;
}
break;
case "queue":
default:
RTC.recent_tags = JSPLib.utility.concatUnique(newtags,RTC.recent_tags);
}
RTC.recent_tags = RTC.recent_tags.slice(0,RTC.user_settings.maximum_tags);
JSPLib.storage.setStorageData('rtc-recent-tags',RTC.recent_tags,localStorage);
RTC.channel.postMessage({type: "reload_recent", recent_tags: RTC.recent_tags, pinned_tags: RTC.pinned_tags, other_recent: RTC.other_recent, new_recent_tags: newtags});
}
////Frequent tags
async function LoadFrequentTags() {
if (!RTC.userid) {
//User must have an account to have frequent tags
return;
}
if (RTC.user_settings.cache_frequent_tags) {
RTC.frequent_tags = JSPLib.storage.checkStorageData('rtc-frequent-tags',ValidateProgramData,localStorage,[]);
if (JSPLib.concurrency.checkTimeout('rtc-frequent-tags-expires',frequent_tags_expires)) {
if (JSPLib.concurrency.reserveSemaphore(PROGRAM_SHORTCUT, 'frequent')) {
await QueryFrequentTags();
JSPLib.concurrency.freeSemaphore(PROGRAM_SHORTCUT, 'frequent');
} else {
return false;
}
}
} else {
if (RTC.controller === 'posts' && RTC.action === 'show') {
await new Promise((resolve)=>{
$(document).one("rtc:get-frequent-tags", () => resolve(null));
});
}
await QueryFrequentTags();
}
return true;
}
async function QueryFrequentTags() {
let user_account = await JSPLib.danbooru.submitRequest('users',{search: {id: RTC.userid}, only: user_fields, expires_in: '300s'});
if (!user_account || user_account.length === 0) {
//Should never get here, but just in case
return;
}
RTC.frequent_tags = user_account[0].favorite_tags.split(/\s+/).map((tag) => tag.trim());
this.debug('log',"Found tags:",RTC.frequent_tags);
if (RTC.user_settings.cache_frequent_tags) {
JSPLib.storage.setStorageData('rtc-frequent-tags',RTC.frequent_tags,localStorage);
JSPLib.concurrency.setRecheckTimeout('rtc-frequent-tags-expires',frequent_tags_expires);
} else {
JSPLib.storage.removeStorageData('rtc-frequent-tags', localStorage);
}
RTC.channel.postMessage({type: "reload_frequent", frequent_tags: RTC.frequent_tags});
}
async function CheckAllFrequentTags() {
let status = await LoadFrequentTags();
if (!status) {
return;
}
if (JSPLib.concurrency.reserveSemaphore(PROGRAM_SHORTCUT, 'frequent')) {
RTC.missing_frequent_tags = await CheckMissingTags(RTC.frequent_tags, "Frequent");
JSPLib.concurrency.freeSemaphore(PROGRAM_SHORTCUT, 'frequent');
} else {
RecheckDisplaySemaphore("frequent");
}
}
////Other
function CleanupTasks() {
JSPLib.storage.pruneEntries(PROGRAM_SHORTCUT, PROGRAM_DATA_REGEX, prune_expires);
}
//Initialization functions
function InitializeRelatedTagColumns() {
$(".user-related-tags-columns").before(`${usertag_columns_html}
`);
if (RTC.controller === 'posts' && RTC.action === 'show') {
// Load the AI tags as well
Danbooru.RelatedTag.initialize_recent_and_favorite_tags();
}
}
function RebindShowRelatedTags() {
if (RTC.controller === 'posts' && RTC.action === 'show') {
JSPLib.utility.recheckTimer({
check: () => JSPLib.utility.isGlobalFunctionBound('danbooru:show-related-tags'),
exec: ()=> {
RTC.cached_data = true;
let old_handlers = JSPLib.utility.saveEventHandlers(document, 'danbooru:show-related-tags');
$(document).off("danbooru:show-related-tags");
if (!RTC.IAC.cached_data) {
$(document).one("danbooru:show-related-tags", Danbooru.Upload.fetch_data_manual);
}
let timer = JSPLib.utility.initializeInterval(()=>{
if (!JSPLib.utility.isNamespaceBound(document, 'danbooru:show-related-tags', 'rtc')) {
$(document).one("danbooru:show-related-tags.rtc", () => {
$(document).trigger("rtc:get-frequent-tags");
clearInterval(timer);
});
} else if (JSPLib.utility.getProgramTime() > (JSPLib.utility.one_second * 10)) {
clearInterval(timer);
}
}, 100);
JSPLib.utility.rebindEventHandlers(document, "danbooru:show-related-tags", old_handlers, ['iac']);
}
}, 100);
}
}
//Settings functions
function BroadcastRTC(ev) {
this.debug('log',`(${ev.data.type}):`, ev.data);
switch (ev.data.type) {
case "update_category":
JSPLib.storage.batchStorageCheck(ev.data.network_tags, ValidateEntry, tag_expires, 'tag').then(()=>{
ev.data.network_tags.forEach((tag)=>{
let category = GetTagCategory(tag);
let escaped_tag = JSPLib.utility.HTMLEscape(tag);
$(`li.tag-type-${notfound_tag_category} a[href$="${escaped_tag}"]`).closest('li').removeClass().addClass(`tag-type-${category}`);
});
});
break;
case "reload_recent":
RTC.pinned_tags = ev.data.pinned_tags;
RTC.recent_tags = ev.data.recent_tags;
RTC.other_recent = ev.data.other_recent;
!RTC.is_setting_menu && RecheckAndDisplay("recent");
break;
case "reload_frequent":
RTC.frequent_tags = ev.data.frequent_tags;
!RTC.is_setting_menu && RecheckAndDisplay("frequent");
//falls through
default:
//do nothing
}
}
function RemoteSettingsCallback() {
RTC.tag_order = GetTagOrderType();
}
function GetTagOrderType() {
if (RTC.is_upload) {
return RTC.user_settings.uploads_order[0];
} else {
return RTC.user_settings.post_edits_order[0];
}
}
function InitializeProgramValues() {
Object.assign(RTC, {
userid: Danbooru.CurrentUser.data('id'),
is_upload: (RTC.controller === 'uploads' && RTC.action === 'show') || (RTC.controller === 'upload-media-assets' && RTC.action === 'show'),
is_setting_menu: JSPLib.danbooru.isSettingMenu(),
});
Object.assign(RTC, {
tag_order: GetTagOrderType(),
preedittags: GetTagList(),
starting_tags: GetStartingTags(),
recent_tags: JSPLib.storage.checkStorageData('rtc-recent-tags', ValidateProgramData,localStorage, []),
pinned_tags: JSPLib.storage.checkStorageData('rtc-pinned-tags', ValidateProgramData,localStorage, []),
});
if (RTC.user_settings.list_type[0] === "multiple") {
RTC.other_recent = JSPLib.storage.checkStorageData('rtc-other-recent',ValidateProgramData,localStorage,[]);
} else {
JSPLib.storage.removeStorageData('rtc-other-recent', localStorage);
JSPLib.storage.removeStorageData('rtc-was-upload', localStorage);
}
RTC.pageload_recentcheck = CheckAllRecentTags();
RTC.pageload_frequentcheck = CheckAllFrequentTags();
JSPLib.load.setProgramGetter(RTC, 'IAC', 'IndexedAutocomplete');
return true;
}
function RenderSettingsMenu() {
$("#recent-tags-calc").append(JSPLib.menu.renderMenuFramework(MENU_CONFIG));
$("#rtc-general-settings").append(JSPLib.menu.renderDomainSelectors());
$('#rtc-order-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", ORDER_SETTINGS_DETAILS));
$("#rtc-order-settings").append(JSPLib.menu.renderInputSelectors('uploads_order','radio'));
$("#rtc-order-settings").append(JSPLib.menu.renderInputSelectors('post_edits_order','radio'));
$("#rtc-order-settings").append(JSPLib.menu.renderCheckbox('metatags_first'));
$("#rtc-order-settings").append(JSPLib.menu.renderCheckbox('aliases_first'));
$("#rtc-order-settings").append(JSPLib.menu.renderSortlist('category_order'));
$('#rtc-list-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", LIST_SETTINGS_DETAILS));
$("#rtc-list-settings").append(JSPLib.menu.renderInputSelectors('list_type','radio'));
$("#rtc-list-settings").append(JSPLib.menu.renderTextinput('maximum_tags',5));
$("#rtc-list-settings").append(JSPLib.menu.renderTextinput('maximum_tag_groups',5));
$('#rtc-inclusion-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", INCLUSION_SETTINGS_DETAILS));
$("#rtc-inclusion-settings").append(JSPLib.menu.renderCheckbox('include_metatags'));
$("#rtc-inclusion-settings").append(JSPLib.menu.renderCheckbox('include_unchanged_tags'));
$("#rtc-inclusion-settings").append(JSPLib.menu.renderCheckbox('include_removed_tags'));
$("#rtc-inclusion-settings").append(JSPLib.menu.renderCheckbox('include_deleted_tags'));
$('#rtc-frequent-settings-message').append(JSPLib.menu.renderExpandable("Additional setting details", FREQUENT_SETTINGS_DETAILS));
$("#rtc-frequent-settings").append(JSPLib.menu.renderCheckbox('cache_frequent_tags'));
$("#rtc-frequent-controls").append(JSPLib.menu.renderLinkclick('refresh_frequent_tags', true));
$('#rtc-controls').append(JSPLib.menu.renderCacheControls());
$('#rtc-cache-controls-message').append(JSPLib.menu.renderExpandable("Cache Data details", CACHE_DATA_DETAILS));
$("#rtc-cache-controls").append(JSPLib.menu.renderLinkclick('cache_info', true));
$('#rtc-cache-controls').append(JSPLib.menu.renderCacheInfoTable());
$("#rtc-cache-controls").append(JSPLib.menu.renderLinkclick('purge_cache', true));
$('#rtc-controls').append(JSPLib.menu.renderCacheEditor(true));
$('#rtc-cache-editor-message').append(JSPLib.menu.renderExpandable("Program Data details", PROGRAM_DATA_DETAILS));
$("#rtc-cache-editor-controls").append(JSPLib.menu.renderKeyselect('data_source', true));
$("#rtc-cache-editor-controls").append(JSPLib.menu.renderDataSourceSections());
$("#rtc-section-indexed-db").append(JSPLib.menu.renderKeyselect('data_type', true));
$("#rtc-section-local-storage").append(JSPLib.menu.renderCheckbox('raw_data', true));
$("#rtc-cache-editor-controls").append(JSPLib.menu.renderTextinput('data_name', 20, true));
JSPLib.menu.engageUI(true,true);
disabled_order_types.forEach((type)=>{
$(`#rtc-select-uploads-order-${type}`).checkboxradio("disable");
$(`#rtc-select-post-edits-order-${type}`).checkboxradio("disable");
});
$("#rtc-control-refresh-frequent-tags").on(PROGRAM_CLICK, ReloadFrequentTags);
JSPLib.menu.saveUserSettingsClick();
JSPLib.menu.resetUserSettingsClick(localstorage_keys);
JSPLib.menu.cacheInfoClick();
JSPLib.menu.purgeCacheClick();
JSPLib.menu.expandableClick();
JSPLib.menu.dataSourceChange();
JSPLib.menu.rawDataChange();
JSPLib.menu.getCacheClick(ValidateProgramData);
JSPLib.menu.saveCacheClick(ValidateProgramData, ValidateEntry);
JSPLib.menu.deleteCacheClick();
JSPLib.menu.listCacheClick();
JSPLib.menu.refreshCacheClick();
JSPLib.menu.cacheAutocomplete();
}
//Main program
function Main() {
this.debug('log',"Initialize start:", JSPLib.utility.getProgramTime());
const preload = {
run_on_settings: false,
default_data: DEFAULT_VALUES,
initialize_func: InitializeProgramValues,
broadcast_func: BroadcastRTC,
menu_css: MENU_CSS,
};
if (!JSPLib.menu.preloadScript(RTC, RenderSettingsMenu, preload)) return;
RebindShowRelatedTags();
InitializeRelatedTagColumns();
DisplayRecentTags();
DisplayFrequentTags();
$("#form").on('submit.rtc', CaptureTagSubmission);
JSPLib.utility.setCSSStyle(program_css,'program');
JSPLib.statistics.addPageStatistics(PROGRAM_NAME);
JSPLib.load.noncriticalTasks(CleanupTasks);
}
/****Function decoration****/
[
Main,BroadcastRTC,QueryFrequentTags,CaptureTagSubmission,SortTagData,FilterDeletedTags,
QueryMissingTags,CheckMissingTags,RecheckDisplaySemaphore,ValidateEntry,
] = JSPLib.debug.addFunctionLogs([
Main,BroadcastRTC,QueryFrequentTags,CaptureTagSubmission,SortTagData,FilterDeletedTags,
QueryMissingTags,CheckMissingTags,RecheckDisplaySemaphore,ValidateEntry,
]);
[
RenderSettingsMenu,CaptureTagSubmission,
CheckAllRecentTags,CheckAllFrequentTags,QueryFrequentTags,CheckMissingTags,
] = JSPLib.debug.addFunctionTimers([
//Sync
RenderSettingsMenu,CaptureTagSubmission,
//Async
CheckAllRecentTags,CheckAllFrequentTags,QueryFrequentTags,
[CheckMissingTags, 1],
]);
/****Initialization****/
//Variables for debug.js
JSPLib.debug.debug_console = false;
JSPLib.debug.level = JSPLib.debug.INFO;
JSPLib.debug.program_shortcut = PROGRAM_SHORTCUT;
//Variables for menu.js
JSPLib.menu.program_shortcut = PROGRAM_SHORTCUT;
JSPLib.menu.program_name = PROGRAM_NAME;
JSPLib.menu.program_data = RTC;
JSPLib.menu.program_reset_data = PROGRAM_RESET_KEYS;
JSPLib.menu.program_data_regex = PROGRAM_DATA_REGEX;
JSPLib.menu.program_data_key = PROGRAM_DATA_KEY;
JSPLib.menu.settings_callback = RemoteSettingsCallback;
JSPLib.menu.reset_callback = RemoteSettingsCallback;
JSPLib.menu.settings_config = SETTINGS_CONFIG;
JSPLib.menu.control_config = CONTROL_CONFIG;
//Export JSPLib
JSPLib.load.exportData(PROGRAM_NAME, RTC, {datalist: ['cached_data']});
/****Execution start****/
JSPLib.load.programInitialize(Main, {program_name: PROGRAM_NAME, required_variables: PROGRAM_LOAD_REQUIRED_VARIABLES, optional_selectors: PROGRAM_LOAD_OPTIONAL_SELECTORS});