// ==UserScript==
// @name CurrentUploads
// @namespace https://github.com/BrokenEagle/JavaScripts
// @version 16.25
// @description Gives up-to-date stats on uploads.
// @source https://danbooru.donmai.us/users/23799
// @author BrokenEagle
// @match https://*.donmai.us/*
// @exclude /^https://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/
// @run-at document-idle
// @downloadURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/CurrentUploads.user.js
// @updateURL https://raw.githubusercontent.com/BrokenEagle/JavaScripts/master/CurrentUploads.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://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.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/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 CanvasJS */
/****Library updates****/
////NONE
/****Global variables****/
//Exterior script variables
const DANBOORU_TOPIC_ID = '15169';
//Variables for load.js
const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery', 'window.Danbooru', 'Danbooru.CurrentUser'];
const PROGRAM_LOAD_REQUIRED_SELECTORS = ["#top", "#page-footer"];
//Program name constants
const PROGRAM_SHORTCUT = 'cu';
const PROGRAM_NAME = 'CurrentUploads';
//Program data constants
const PROGRAM_DATA_REGEX = /^rti-|ct(d|w|mo|y|at)-|(daily|weekly|monthly|yearly|alltime|previous)-(uploads|approvals)-/; //Regex that matches the prefix of all program cache data
//Main program variable
const CU = {};
//For factory reset
const LOCALSTORAGE_KEYS = [
'cu-current-metric',
'cu-hide-current-uploads',
'cu-stash-current-uploads'
];
const PROGRAM_RESET_KEYS = {
checked_usernames: {},
checked_users: {user: {}, approver: {}},
user_copytags: {user: {}, approver: {}},
period_available: {user: {}, approver: {}},
reverse_implications: {},
};
//Available setting values
const period_selectors = ['daily', 'weekly', 'monthly', 'yearly', 'alltime'];
//Main settings
const SETTINGS_CONFIG = {
copyrights_merge: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Merge all implied copyrights to their base copyright. Ex: (splatoon_1, splatoon_2) -> splatoon."
},
copyrights_enabled: {
reset: true,
validate: JSPLib.utility.isBoolean,
hint: "Process and show user copyright uploads."
},
periods_shown: {
allitems: period_selectors,
reset: period_selectors,
validate: (data) => (JSPLib.menu.validateCheckboxRadio(data, 'checkbox', period_selectors) && data.includes('daily')),
hint: "Select which periods to process and show."
},
copyrights_threshold: {
reset: 0,
parse: parseInt,
validate: (data) => (Number.isInteger(data) && data >= 0),
hint: "Maximum number of copyrights to display. Enter 0 to disable this threshold."
},
postcount_threshold: {
reset: 0,
parse: parseInt,
validate: (data) => (Number.isInteger(data) && data >= 0),
hint: "Minimum postcount to display copyright. Enter 0 to disable this threshold."
}
};
//Available config values
const all_source_types = ['indexed_db', 'local_storage'];
const all_data_types = ['count', 'uploads', 'approvals', 'reverse_implication', 'custom'];
const all_periods = ['daily', 'weekly', 'monthly', 'yearly', 'alltime', 'previous'];
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: 'count',
hint: "Select type of data. Use Custom for querying by keyname.",
},
data_period: {
allitems: all_periods,
value: "",
hint: "Select the data period. Count cannot use the 'Previous' period.",
},
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: 'display',
}],
controls: [],
};
// Default values
const DEFAULT_VALUES = {
usertag: 'user',
counttype: 'uploads',
controls_initialized: false,
copyright_period: 'd',
};
//CSS Constants
//Style information
const PROGRAM_CSS = `
#upload-counts {
border-style: dotted;
border-width: 2px;
max-width: 70em;
margin-left: 2em;
}
#upload-counts.opened {
border-style: dashed;
border-width: 5px;
}
#upload-counts.stashed {
display: none;
}
#count-module {
margin-bottom: 1em;
display: none;
border: var(--form-input-border);
}
#upload-counts.opened #count-module {
display: block;
}
#count-header {
margin-left: 1em;
}
#count-table {
white-space: nowrap;
margin-left: 1em;
}
#count-header th,
#count-table td {
width: 10em;
text-align: center;
}
#count-header th:first-of-type,
#count-table td:first-of-type {
width: 12em;
text-align: left;
}
#count-table.overflowed {
max-height: 20em;
overflow-x: hidden;
overflow-y: auto;
}
#count-order {
color: var(--muted-text-color);
font-style: italic;
margin-right: 4em;
font-size: 70%;
text-align: right;
}
#count-chart {
height: 400px;
width: 100%;
display: none;
}
#count-controls {
display: none;
margin-left: 1em;
}
#count-query-user {
margin: 0.5em;
display: flex;
gap: 0.5em;
}
#count-query-user > label {
background-color: var(--grey-2);
padding: 5px 10px;
border-radius: 25px;
border: 1px solid grey;
cursor: pointer;
}
#count-query-user > label:hover {
background-color: var(--grey-1);
}
#count-query-user > label > input {
vertical-align: middle;
}
#stash-count-notice {
color: #F44;
font-weight: bold;
font-size: 80%;
}
#stash-count-notice:hover {
color: #F88;
}
#empty-uploads {
margin: 1em;
font-size: 200%;
font-weight: bold;
font-family: monospace;
}
#upload-counts.opened #upload-counts-toggle {
margin: 0.5em;
}
#upload-counts-restore {
display: none;
}
#upload-counts-restore.stashed {
display: inline-block;
}
#upload-counts-restore a {
color: mediumseagreen;
}
#restore-count-notice:hover {
filter: brightness(1.1);
}
.cu-tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black;
min-width: 2em;
text-align: center;
}
.cu-tooltip .cu-tooltiptext {
visibility: hidden;
width: 90px;
background-color: black;
color: #fff;
text-align: left;
border-radius: 6px;
padding: 5px;
/* Position the tooltip */
position: absolute;
z-index: 1;
top: -50px;
right: -100px;
}
.cu-tooltip:hover .cu-tooltiptext.cu-activetooltip {
visibility: visible;
}
#count-table.overflowed tr:nth-child(1) .cu-tooltiptext {
top: -5px;
}
#count-table.overflowed tr:nth-child(2) .cu-tooltiptext {
top: -25px;
}
#count-table.overflowed tr:nth-child(3) .cu-tooltiptext {
top: -40px;
}
#count-table.overflowed tr:nth-last-child(2) .cu-tooltiptext {
top: -60px;
}
#count-table.overflowed tr:nth-last-child(1) .cu-tooltiptext {
top: -75px;
}
.cu-select-tooltip a {
color: grey;
margin-right: 1em;
}
.cu-select-tooltip a:hover,
.cu-select-period a:hover{
filter: brightness(1.5);
}
.cu-select-tooltip.cu-activetooltip a {
font-weight: bold;
}
.cu-period-header {
background-color: #CCC;
border-left: 1px solid #444;
margin-left: -1px;
}
[data-current-user-theme="dark"] .cu-period-header {
background-color: #666;
}
#count-header .cu-manual,
#count-header .cu-limited {
background-color: var(--body-background-color);
}
#count-header .cu-manual:hover,
#count-header .cu-limited:hover {
color: grey;
}
#count-table .cu-uploads {
background-color: var(--body-background-color);
padding: 0 5px;
}
#count-table a.with-style:hover {
filter: brightness(1.5);
}
#count-copyrights {
margin: 1em;
display: none;
}
#count-copyrights-header {
font-size: 1.25em;
font-weight: bold;
display: flex;
}
#count-copyrights-section {
margin: 0.5em;
display: none;
}
.cu-select-period a {
color: grey;
margin-right: 1em;
}
.cu-select-period.cu-active-period a {
font-weight: bold;
}
#count-copyrights-list {
line-height: 150%;
}
#count-copyrights-list .cu-active-copyright a {
background: #0073ff;
color: #FFF;
}
#empty-statistics {
margin: 1em;
font-weight: bold;
font-size: 16px;
}
#count-copyrights-manual {
margin: 1em;
display: none;
}
#count-copyrights-header .cu-triangle {
position: relative;
}
#count-copyrights-header .cu-svg-caret {
position: absolute;
}
#count-copyrights-header .cu-triangle-right .cu-svg-caret-down {
display: none;
}
#count-copyrights-header .cu-triangle-down .cu-svg-caret-right {
display: none;
}
.cu-svg-icon {
fill: var(--text-color);
}
#upload-counts {
border-color: var(--grey-1);
}
#upload-counts.opened {
border-color: var(--grey-2);
}
#count-table .cu-manual,
#count-table .cu-limited {
background-color: lightcyan;
border-left: 1px solid #CCC;
}
body[data-current-user-theme=dark] #upload-counts {
border-color: var(--grey-8);
}
body[data-current-user-theme=dark] #upload-counts.opened {
border-color: var(--grey-7);
}
body[data-current-user-theme="dark"] #count-table .cu-manual,
body[data-current-user-theme="dark"] #count-table .cu-limited {
background-color: darkcyan;
}
@media (prefers-color-scheme: dark) {
body[data-current-user-theme=auto] #upload-counts {
border-color: var(--grey-8);
}
body[data-current-user-theme=auto] #upload-counts.opened {
border-color: var(--grey-7);
}
body[data-current-user-theme="auto"] #count-table .cu-manual,
body[data-current-user-theme="auto"] #count-table .cu-limited {
background-color: darkcyan;
}
}`;
//HTML constants
const CARET_RIGHT = ' ';
const CARET_DOWN = ' ';
const notice_box = `
`;
const unstash_notice = ` - Restore ${PROGRAM_NAME} `;
const copyright_counter = '(... )';
const CACHE_DATA_DETAILS = `
Count data (ctd,ctw,ctmo,cty,ctat): Main data shown in the table.
Post data: Used to determine post statistics shown in the tooltips and chart data.
Key format: (daily|weekly|monthly|yearly|alltime|previous)-(uploads|approvals)-USERNAME
For daily , weekly , monthly and previous , the data represents actual post values.
The data has been compressed into an array to save space, but the following is what each index represents:
0. Post ID
1. Score
2. Upscore
3. Downscore
4. Favcount
5. Tagcount
6. Gentags
7. Copyrights string
8. Created timestamp
For yearly and alltime , the data represents the finalized statistics and chart data.
Reverse tag implications (rti): The number of tags that a tag implicates. Used to determine the base copyright tag.
`;
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.
Status data
current-metric: Which metric to show upon first opening the table.
hide-current-uploads: Should the table be opened on page load?
stash-current-uploads: Should the table be hidden and the restore link shown? Takes precedence over hide-current-uploads.
`;
//Time periods
const timevalues = ['d', 'w', 'mo', 'y', 'at'];
const manual_periods = ['w', 'mo'];
const limited_periods = ['y', 'at'];
const copyright_periods = ['d', 'w', 'mo'];
//Period constants
const period_info = {
countexpires: {
d: 5 * JSPLib.utility.one_minute,
w: JSPLib.utility.one_hour,
mo: JSPLib.utility.one_day,
y: JSPLib.utility.one_week,
at: JSPLib.utility.one_month
},
uploadexpires: {
d: 5 * JSPLib.utility.one_minute,
w: JSPLib.utility.one_day,
mo: JSPLib.utility.one_week,
y: JSPLib.utility.one_month,
at: JSPLib.utility.one_year
},
longname: {
d: 'daily',
w: 'weekly',
mo: 'monthly',
y: 'yearly',
at: 'alltime'
},
header: {
d: 'Day',
w: 'Week',
mo: 'Month',
y: 'Year',
at: 'All-time'
},
points: {
w: 7,
mo: 30,
y: 12,
at: 0
},
xlabel: {
w: "Days ago",
mo: "Days ago",
y: "Months ago",
at: "Months ago"
},
divisor: {
w: JSPLib.utility.one_day,
mo: JSPLib.utility.one_day,
y: JSPLib.utility.one_month,
at: JSPLib.utility.one_month,
}
};
const longname_key = {
daily: 'd',
weekly: 'w',
monthly: 'mo',
yearly: 'y',
alltime: 'at'
};
//Time constants
const prune_expires = JSPLib.utility.one_day;
const rti_expiration = JSPLib.utility.one_month;
const JQUERY_DELAY = 1; //For jQuery updates that should not be done synchronously
//Network call configuration
const max_post_limit_query = 100;
//Metrics used by statistics functions
const tooltip_metrics = ['score', 'upscore', 'downscore', 'favcount', 'tagcount', 'gentags', 'week', 'day'];
const chart_metrics = ['score', 'upscore', 'downscore', 'favcount', 'tagcount', 'gentags'];
//Feedback messages
const empty_uploads_message_owner = 'Feed me more uploads!';
const empty_uploads_message_other = 'No uploads for this user.';
const empty_approvals_message_other = 'No approvals for this user.';
const empty_uploads_message_anonymous = 'User is Anonymous, so no uploads.';
const copyright_no_uploads = 'No uploads, so no copyrights available for this period.';
const copyright_no_statistics = 'No statistics available for this period (click the table header ).';
//Other constants
const name_field = "name";
const id_field = "id";
const user_fields = "name,level_string";
const post_fields = "id,score,up_score,down_score,fav_count,tag_count,tag_count_general,tag_string_copyright,created_at";
//Validation values
const validation_constraints = {
countentry: JSPLib.validate.counting_constraints,
implicationentry: JSPLib.validate.counting_constraints,
postentries: JSPLib.validate.array_constraints,
statentries: JSPLib.validate.hash_constraints,
postentry: [
JSPLib.validate.integer_constraints, //ID
JSPLib.validate.integer_constraints, //SCORE
JSPLib.validate.integer_constraints, //UPSCORE
JSPLib.validate.integer_constraints, //DOWNSCORE
JSPLib.validate.integer_constraints, //FAVCOUNT
JSPLib.validate.integer_constraints, //TAGCOUNT
JSPLib.validate.integer_constraints, //GENTAGS
JSPLib.validate.stringonly_constraints, //COPYRIGHTS
JSPLib.validate.integer_constraints //CREATED
],
postmetric: {
chart_data: JSPLib.validate.hash_constraints,
score: JSPLib.validate.hash_constraints,
upscore: JSPLib.validate.hash_constraints,
downscore: JSPLib.validate.hash_constraints,
favcount: JSPLib.validate.hash_constraints,
tagcount: JSPLib.validate.hash_constraints,
gentags: JSPLib.validate.hash_constraints,
week: JSPLib.validate.array_constraints,
day: JSPLib.validate.array_constraints
},
timestat: JSPLib.validate.basic_number_validator,
poststat: {
max: JSPLib.validate.integer_constraints,
average: JSPLib.validate.number_constraints,
stddev: JSPLib.validate.number_constraints,
outlier: JSPLib.validate.integer_constraints,
adjusted: JSPLib.validate.number_constraints
},
chartentry: {
score: JSPLib.validate.array_constraints,
upscore: JSPLib.validate.array_constraints,
downscore: JSPLib.validate.array_constraints,
favcount: JSPLib.validate.array_constraints,
tagcount: JSPLib.validate.array_constraints,
gentags: JSPLib.validate.array_constraints,
uploads: JSPLib.validate.array_constraints
},
chartdata: {
x: JSPLib.validate.integer_constraints,
y: JSPLib.validate.number_constraints
}
};
/**FUNCTIONS**/
//Validation functions
function BuildValidator(validation_key) {
return {
expires: JSPLib.validate.expires_constraints,
value: validation_constraints[validation_key]
};
}
function ValidateEntry(key, entry) {
let printer = JSPLib.debug.getFunctionPrint('ValidateEntry');
if (!JSPLib.validate.validateIsHash(key, entry)) {
return false;
}
if (key.match(/^ct(d|w|mo|y|at)?-/)) {
return JSPLib.validate.validateHashEntries(key, entry, BuildValidator('countentry'));
}
if (key.match(/^rti-/)) {
return JSPLib.validate.validateHashEntries(key, entry, BuildValidator('implicationentry'));
}
if (key.match(/^(daily|weekly|monthly|previous)-(uploads|approvals)-/)) {
if (!JSPLib.validate.validateHashEntries(key, entry, BuildValidator('postentries'))) {
return false;
}
return ValidatePostentries(key + '.value', entry.value);
}
if (key.match(/^(yearly|alltime)-(uploads|approvals)-/)) {
if (!JSPLib.validate.validateHashEntries(key, entry, BuildValidator('statentries'))) {
return false;
}
return ValidateStatEntries(key + '.value', entry.value);
}
printer.debuglog("Bad key!");
return false;
}
function ValidatePostentries(key, postentries) {
for (let i = 0;i < postentries.length;i++){
let value_key = key + `[${i}]`;
if (!JSPLib.validate.validateIsArray(value_key, postentries[i], {is: validation_constraints.postentry.length})) {
return false;
}
//It's technically not a hash, although it works since arrays can be treated like one
if (!JSPLib.validate.validateHashEntries(value_key, postentries[i], validation_constraints.postentry)) {
return false;
}
}
return true;
}
function ValidateStatEntries(key, statentries) {
if (!JSPLib.validate.validateHashEntries(key, statentries, validation_constraints.postmetric)) {
return false;
}
for (let i = 0; i < tooltip_metrics.length; i++) {
let metric = tooltip_metrics[i];
let metric_key = key + '.' + metric;
if (metric === 'week' || metric === 'day') {
if (!JSPLib.validate.validateArrayValues(metric_key, statentries[metric], validation_constraints.timestat)) {
return false;
}
} else if (!JSPLib.validate.validateHashEntries(metric_key, statentries[metric], validation_constraints.poststat)) {
return false;
}
}
return ValidateChartEntries(key + '.chart_data', statentries.chart_data);
}
function ValidateChartEntries(key, chartentries) {
if (!JSPLib.validate.validateHashEntries(key, chartentries, validation_constraints.chartentry)) {
return false;
}
for (let chart_key in chartentries) {
for (let i = 0; i < chartentries[chart_key].length; i ++) {
if (!JSPLib.validate.validateHashEntries(`${key}.${chart_key}[${i}]`, chartentries[chart_key][i], validation_constraints.chartdata)) {
return false;
}
}
}
return true;
}
function ValidateProgramData(key, entry) {
var checkerror = [];
switch (key) {
case 'cu-user-settings':
checkerror = JSPLib.menu.validateUserSettings(entry, SETTINGS_CONFIG);
break;
case 'cu-prune-expires':
if (!Number.isInteger(entry)) {
checkerror = ["Value is not an integer."];
}
break;
case 'cu-current-metric':
if (!tooltip_metrics.includes(entry)) {
checkerror = [`Value not in list: ${tooltip_metrics}`];
}
break;
case 'cu-hide-current-uploads':
case 'cu-stash-current-uploads':
if (!JSPLib.utility.isBoolean(entry)) {
checkerror = ['Value is not a boolean.'];
}
break;
default:
checkerror = ["Not a valid program data key."];
}
if (checkerror.length) {
JSPLib.validate.outputValidateError(key, checkerror);
return false;
}
return true;
}
//Table functions
function AddTable(input, inner_args = "") {
return `\r\n';
}
function AddTableHead(input, inner_args = "") {
return `\r\n` + input + ' \r\n';
}
function AddTableBody(input, inner_args = "") {
return `\r\n` + input + ' \r\n';
}
function AddTableRow(input, inner_args = "") {
return `\r\n` + input + ' \r\n';
}
function AddTableHeader(input, inner_args = "") {
return `` + input + ' \r\n';
}
function AddTableData(input, inner_args = "") {
return `` + input + ' \r\n';
}
//Render functions
//Render table
function RenderHeader() {
var tabletext = AddTableHeader('Name');
let click_periods = manual_periods.concat(limited_periods);
let times_shown = GetShownPeriodKeys();
times_shown.forEach((period) => {
let header = period_info.header[period];
if (click_periods.includes(period)) {
let is_available = CU.period_available[CU.usertag][CU.current_username][period];
let link_class = (manual_periods.includes(period) ? 'cu-manual' : 'cu-limited');
let header_class = (!is_available ? 'cu-process' : '');
let counter_html = (!is_available ? ' (... ) ' : '');
tabletext += AddTableHeader(`${header} ${counter_html}`, `class="cu-period-header ${header_class}" data-period="${period}"`);
} else {
tabletext += AddTableHeader(header, `class="cu-period-header" data-period="${period}"`);
}
});
return AddTableHead(AddTableRow(tabletext));
}
function RenderBody() {
if (CU.active_copytags.length > 5) {
$("#count-table").addClass("overflowed");
} else {
$("#count-table").removeClass("overflowed");
}
var tabletext = RenderRow('');
for (let i = 0;i < CU.active_copytags.length; i++) {
tabletext += RenderRow(CU.active_copytags[i]);
}
return AddTableBody(tabletext);
}
function RenderRow(key) {
var rowtag = (key === '' ? `${CU.usertag}:` + CU.display_username : key);
var rowtext = (key === '' ? CU.display_username : key).replace(/_/g, ' ');
rowtext = JSPLib.utility.maxLengthString(rowtext);
var rowaddon = (key === '' ? `class="user-${CU.level_string.toLowerCase()} with-style"` : 'class="tag-type-3"');
var tabletext = AddTableData(JSPLib.danbooru.postSearchLink(rowtag, rowtext, rowaddon));
let times_shown = GetShownPeriodKeys();
let click_periods = manual_periods.concat(limited_periods);
for (let i = 0;i < times_shown.length; i++) {
let period = times_shown[i];
let data_text = GetTableValue(key, period);
let is_limited = limited_periods.includes(period);
let class_name = (!is_limited ? 'cu-hover' : '');
if (click_periods.includes(period) && key === '') {
class_name += (manual_periods.includes(period) ? ' cu-manual' : ' cu-limited');
}
let rowdata = `class="${class_name}" data-period="${period}"`;
let is_available = CU.period_available[CU.usertag][CU.current_username][period];
if (is_available && is_limited && key === '') {
tabletext += AddTableData(RenderTooltipData(data_text, times_shown[i], true), rowdata);
} else if (is_available && !is_limited) {
tabletext += AddTableData(RenderTooltipData(data_text, times_shown[i]), rowdata);
} else {
tabletext += AddTableData(`${data_text} `, rowdata);
}
}
return AddTableRow(tabletext, `data-key="${key}"`);
}
function RenderOrderMessage(period, sorttype) {
let header = period_info.header[period];
switch (sorttype) {
case 1:
return `Copyrights ordered by user postcount; ${header} period; L -> H`;
case 2:
return `Copyrights ordered by site postcount; ${header} period; H -> L`;
case 3:
return `Copyrights ordered by site postcount; ${header} period; L -> H`;
case 0:
default:
return `Copyrights ordered by user postcount; ${header} period; H -> L`;
}
}
//Get the data and validate it without checking the expires
function GetCountData(key, default_val = null) {
let count_data = JSPLib.storage.getIndexedSessionData(key);
if (!ValidateEntry(key, count_data)) {
return default_val;
}
return count_data.value;
}
function GetTableValue(key, type) {
if (key === '') {
return GetCountData('ct' + type + `-${CU.usertag}:` + CU.current_username, "N/A");
}
var useruploads = GetCountData('ct' + type + `-${CU.usertag}:` + CU.current_username + ' ' + key, "N/A");
var alluploads = GetCountData('ct' + type + '-' + key, "N/A");
return `(${useruploads}/${alluploads})`;
}
//Render copyrights
function RenderCopyrights(period) {
let copytags = CU.user_copytags[CU.usertag][CU.current_username][period].sort();
return copytags.map((copyright) => {
let tag_text = JSPLib.utility.maxLengthString(copyright);
let taglink = JSPLib.danbooru.postSearchLink(copyright, tag_text, 'class="tag-type-3"');
let active = CU.active_copytags.includes(copyright) ? ' class="cu-active-copyright"' : '';
return `${taglink} `;
}).join(' ');
}
function RenderCopyrightControls() {
return copyright_periods.map((period) => {
let period_name = period_info.longname[period];
return `${JSPLib.utility.titleizeString(period_name)} `;
}).join(' ') + 'Manual ';
}
//Render Tooltips
function RenderTooltipData(text, period, limited = false) {
let tooltip_html = RenderAllToolPopups(period, limited);
return `
${text} ${tooltip_html}
`;
}
function RenderAllToolPopups(period, limited) {
return tooltip_metrics.map((metric) => RenderToolpopup(metric, period, limited)).join('');
}
function RenderToolpopup(metric, period, limited) {
let inner_text = (limited ? RenderStatistics('', metric, period, true) : '');
return `
${inner_text} `;
}
function RenderAllTooltipControls() {
return tooltip_metrics.map((metric) => RenderToolcontrol(metric)).join('');
}
function RenderToolcontrol(metric) {
return `
${JSPLib.utility.titleizeString(metric)} `;
}
function RenderStatistics(key, attribute, period, limited = false) {
let period_key = GetPeriodKey(period_info.longname[period]);
let data = JSPLib.storage.getIndexedSessionData(period_key);
if (!data) {
return "No data!";
}
let stat = data.value;
if (!limited) {
let uploads = PostDecompressData(stat);
if (key !== '') {
uploads = uploads.filter((val) => val.copyrights.split(' ').includes(key));
}
//It's possible with their longer expirations for daily copyrights that don't exist in other periods
if (uploads.length === 0) {
return "No data!";
}
stat = GetAllStatistics(uploads, attribute);
} else {
stat = stat[attribute];
}
return RenderAllStats(stat, attribute);
}
function RenderAllStats(stat, attribute) {
switch (attribute) {
case 'week':
return RenderWeeklist(stat);
case 'day':
return RenderDaylist(stat);
default:
return RenderStatlist(stat);
}
}
function RenderWeeklist(stat) {
return `
Sun: ${stat[0]}
Mon: ${stat[1]}
Tue: ${stat[2]}
Wed: ${stat[3]}
Thu: ${stat[4]}
Fri: ${stat[5]}
Sat: ${stat[6]}
`;
}
function RenderDaylist(stat) {
return `
00-04: ${stat[0]}
04-08: ${stat[1]}
08-12: ${stat[2]}
12-16: ${stat[3]}
16-20: ${stat[4]}
20-24: ${stat[5]}
`;
}
function RenderStatlist(stat) {
return `
Max: ${stat.max}
Avg: ${stat.average}
StD: ${stat.stddev}
Out: ${stat.outlier}
Adj: ${stat.adjusted}
`;
}
function GetAllStatistics(posts, attribute) {
switch (attribute) {
case 'week':
return GetWeekStatistics(posts);
case 'day':
return GetDayStatistics(posts);
default:
return GetPostStatistics(posts, attribute);
}
}
function GetWeekStatistics(posts) {
let week_days = new Array(7).fill(0);
posts.forEach((upload) => {
let timeindex = new Date(upload.created).getUTCDay();
week_days[timeindex] += 1;
});
let week_stats = week_days.map((day) => {
let percent = (100 * day / posts.length);
return (percent === 0 || percent === 100 ? percent : JSPLib.utility.setPrecision(percent, 1));
});
return week_stats;
}
function GetDayStatistics(posts) {
let day_hours = new Array(6).fill(0);
posts.forEach((upload) => {
let timeindex = Math.floor(new Date(upload.created).getUTCHours() / 4);
day_hours[timeindex] += 1;
});
let day_stats = day_hours.map((day) => {
let percent = (100 * day / posts.length);
return (percent === 0 || percent === 100 ? percent : JSPLib.utility.setPrecision(percent, 1));
});
return day_stats;
}
function GetPostStatistics(posts, attribute) {
let data = JSPLib.utility.getObjectAttributes(posts, attribute);
let data_max = Math.max(...data);
let data_average = JSPLib.statistics.average(data);
let data_stddev = JSPLib.statistics.standardDeviation(data);
let data_outliers = JSPLib.statistics.removeOutliers(data);
let data_removed = data.length - data_outliers.length;
let data_adjusted = JSPLib.statistics.average(data_outliers);
return {
max: data_max,
average: JSPLib.utility.setPrecision(data_average, 2),
stddev: JSPLib.utility.setPrecision(data_stddev, 2),
outlier: data_removed,
adjusted: JSPLib.utility.setPrecision(data_adjusted, 2)
};
}
function AssignPostIndexes(period, posts, time_offset) {
let points = period_info.points[period];
//Have to do it this way to avoid getting the same object
let periods = JSPLib.utility.arrayFill(points, "[]");
posts.forEach((post) => {
let index = Math.floor((Date.now() - post.created - time_offset) / (period_info.divisor[period]));
index = (points ? Math.min(points - 1, index) : index);
index = Math.max(0, index);
if (index >= periods.length) {
periods = periods.concat(JSPLib.utility.arrayFill(index + 1 - periods.length, "[]"));
}
periods[index].push(post);
});
return periods;
}
function GetPeriodAverages(indexed_posts, metric) {
let period_averages = [];
for (let index in indexed_posts) {
if (!indexed_posts[index].length) continue;
let data_point = {
x: parseInt(index),
y: JSPLib.utility.setPrecision(JSPLib.statistics.average(JSPLib.utility.getObjectAttributes(indexed_posts[index], metric)), 2)
};
period_averages.push(data_point);
}
return period_averages;
}
function GetPeriodPosts(indexed_posts) {
let period_uploads = [];
for (let index in indexed_posts) {
if (!indexed_posts[index].length) continue;
let data_point = {
x: parseInt(index),
y: indexed_posts[index].length
};
period_uploads.push(data_point);
}
return period_uploads;
}
//Helper functions
//Returns a sorted key array from highest to lowest using the length of the array in each value
function SortDict(dict) {
var items = Object.keys(dict).map((key) => [key, dict[key].length]);
items.sort((first, second) => (first[1] !== second[1] ? second[1] - first[1] : first[0].localeCompare(second[0])));
return items.map((entry) => entry[0]);
}
function BuildTagParams(type, tag) {
return (type === 'at' ? '' : ('age:..1' + type + ' ')) + tag;
}
function GetCopyrightCount(posts) {
let copyright_count = {};
posts.forEach((post) => {
post.copyrights.split(' ').forEach((tag) => {
copyright_count[tag] = copyright_count[tag] || [];
copyright_count[tag] = copyright_count[tag].concat([post.id]);
});
});
if (CU.user_settings.postcount_threshold) {
for (let copyright in copyright_count) {
if (copyright_count[copyright].length < CU.user_settings.postcount_threshold) {
delete copyright_count[copyright];
}
}
}
return copyright_count;
}
function CompareCopyrightCounts(dict1, dict2) {
let difference = [];
JSPLib.utility.arrayUnion(Object.keys(dict1), Object.keys(dict2)).forEach((key) => {
if (!JSPLib.utility.arrayEquals(dict1[key], dict2[key])) {
difference.push(key);
}
});
return difference;
}
function CheckCopyrightVelocity(tag) {
var dayuploads = JSPLib.storage.getIndexedSessionData('ctd-' + tag);
var weekuploads = JSPLib.storage.getIndexedSessionData('ctw-' + tag);
if (dayuploads === null || weekuploads === null) {
return true;
}
var day_gettime = dayuploads.expires - period_info.countexpires.d; //Time data was originally retrieved
var week_velocity = (JSPLib.utility.one_week) / (weekuploads.value | 1); //Milliseconds per upload
var adjusted_poll_interval = Math.min(week_velocity, JSPLib.utility.one_day); //Max wait time is 1 day
return Date.now() > day_gettime + adjusted_poll_interval;
}
async function MergeCopyrightTags(user_copytags) {
let query_implications = JSPLib.utility.arrayDifference(user_copytags, Object.keys(CU.reverse_implications));
let promise_array = query_implications.map((key) => GetReverseTagImplication(key));
let reverse_implications = await Promise.all(promise_array);
query_implications.forEach((key, i) => {
CU.reverse_implications[key] = reverse_implications[i];
});
return user_copytags.filter((value) => (CU.reverse_implications[value] === 0));
}
function IsMissingTag(tag) {
return GetShownPeriodKeys().reduce((total, period) => (total || !GetCountData(`ct${period}-${tag}`)), false);
}
function MapPostData(posts) {
return posts.map((entry) => (
{
id: entry.id,
score: entry.score,
upscore: entry.up_score,
downscore: -entry.down_score,
favcount: entry.fav_count,
tagcount: entry.tag_count,
gentags: entry.tag_count_general,
copyrights: entry.tag_string_copyright,
created: new Date(entry.created_at).getTime()
}
));
}
function PreCompressData(posts) {
return posts.map((entry) => [entry.id, entry.score, entry.upscore, entry.downscore, entry.favcount, entry.tagcount, entry.gentags, entry.copyrights, entry.created]);
}
function PostDecompressData(posts) {
return posts.map((entry) => (
{
id: entry[0],
score: entry[1],
upscore: entry[2],
downscore: entry[3],
favcount: entry[4],
tagcount: entry[5],
gentags: entry[6],
copyrights: entry[7],
created: entry[8]
}
));
}
function GetTagData(tag) {
return Promise.all(CU.user_settings.periods_shown.map((period) => GetCount(longname_key[period], tag)));
}
function GetPeriodKey(period_name) {
return `${period_name}-${CU.counttype}-${CU.current_username}`;
}
function CheckPeriodUploads() {
let promise_array = [];
const checkPeriod = (key, period, check) => {
CU.period_available[CU.usertag][CU.current_username][period] = Boolean(check);
if (!check) {
JSPLib.storage.removeIndexedSessionData(key);
}
};
CU.period_available[CU.usertag][CU.current_username] = CU.period_available[CU.usertag][CU.current_username] || {};
let times_shown = GetShownPeriodKeys();
for (let i = 0; i < times_shown.length; i++) {
let period = times_shown[i];
if (period in CU.period_available[CU.usertag][CU.current_username]) {
continue;
}
let data_key = GetPeriodKey(period_info.longname[period]);
let max_expires = period_info.uploadexpires[period];
let check_promise = JSPLib.storage.checkLocalDB(data_key, max_expires).then((check) => {checkPeriod(data_key, period, check);});
promise_array.push(check_promise);
}
return Promise.all(promise_array);
}
async function PopulateTable() {
//Prevent function from being reentrant while processing uploads
PopulateTable.is_started = true;
var post_data = [];
InitializeControls();
if (CU.checked_users[CU.usertag][CU.current_username] === undefined) {
TableMessage(`Loading data... (... )
`);
post_data = await ProcessUploads(CU.current_username);
CU.checked_users[CU.usertag][CU.current_username] = post_data.length;
}
let is_override = $("#count_override_select")[0].checked;
if (is_override || CU.checked_users[CU.usertag][CU.current_username]) {
CU.active_copytags = JSPLib.utility.dataCopy(CU.user_copytags[CU.usertag][CU.current_username].daily);
await CheckPeriodUploads(CU.current_username);
InitializeTable();
} else {
TableMessage(`${CU.empty_uploads_message}
`);
}
PopulateTable.is_started = false;
}
function InitializeControls() {
//Render the controls only once when the table is first opened
if (!CU.controls_initialized) {
$("#count-controls").html(RenderAllTooltipControls());
$("#count-copyrights-controls").html(RenderCopyrightControls());
$(".cu-select-tooltip").on(JSPLib.program_click, TooltipChange);
$(".cu-select-period a").on(JSPLib.program_click, CopyrightPeriod);
$("#count-copyrights-header a").on(JSPLib.program_click, ToggleCopyrightsSection);
$("#count_submit_user_id").on(JSPLib.program_click, CheckUser);
$("#count_refresh_user_id").on(JSPLib.program_click, RefreshUser);
$("#count_add_copyright").on(JSPLib.program_click, AddCopyright);
CU.controls_initialized = true;
if (CU.use_IAC) {
setTimeout(() => {
CU.IAC.InitializeAutocompleteIndexed("#count_query_user_id", 'us');
}, 1000);
}
}
}
function InitializeTable() {
$("#count-header").html(AddTable(RenderHeader(), 'class="striped"'));
$("#count-table").html(AddTable(RenderBody(), 'class="striped"'));
$("#count-order").html(RenderOrderMessage("d", 0));
$("#count-header .cu-process").on(JSPLib.program_click, GetPeriod);
$("#count-header th").on(JSPLib.program_click, SortTable);
$("#count-table .cu-manual,#count-table .cu-limited").on(JSPLib.program_click, RenderChart);
$("#count-controls,#count-copyrights,#count-header").show();
$(`.cu-select-tooltip[data-type="${CU.current_metric}"] a`).click();
CU.sorttype = 0;
CU.sortperiod = "d";
if (CU.copyright_period) {
$(`.cu-select-period[data-type="${CU.copyright_period}"] a`).click();
}
CU.shown_copytags = JSPLib.utility.dataCopy(CU.active_copytags);
}
function TableMessage(message) {
$("#count-table").html(message);
$("#count-controls,#count-copyrights,#count-header,#count-chart").hide();
}
//Network functions
async function GetReverseTagImplication(tag) {
let printer = JSPLib.debug.getFunctionPrint('GetReverseTagImplication');
var key = 'rti-' + tag;
var check = await JSPLib.storage.checkLocalDB(key, rti_expiration);
if (!(check)) {
printer.debuglog("Network:", key);
let data = await JSPLib.danbooru.submitRequest('tag_implications', {search: {antecedent_name: tag}, only: id_field}, {default_val: [], key});
JSPLib.storage.saveData(key, {value: data.length, expires: JSPLib.utility.getExpires(rti_expiration)});
return data.length;
}
return check.value;
}
async function GetCount(type, tag) {
let printer = JSPLib.debug.getFunctionPrint('GetCount');
let max_expires = period_info.countexpires[type];
var key = 'ct' + type + '-' + tag;
var check = await JSPLib.storage.checkLocalDB(key, max_expires);
if (!(check)) {
printer.debuglog("Network:", key);
return JSPLib.danbooru.submitRequest('counts/posts', {tags: BuildTagParams(type, tag), skip_cache: true}, {default_val: {counts: {posts: 0}}, key})
.then((data) => {
JSPLib.storage.saveData(key, {value: data.counts.posts, expires: JSPLib.utility.getExpires(max_expires)});
});
}
}
async function GetPeriodUploads(username, period, limited = false, domname = null) {
let printer = JSPLib.debug.getFunctionPrint('GetPeriodUploads');
let period_name = period_info.longname[period];
let max_expires = period_info.uploadexpires[period];
let key = GetPeriodKey(period_name);
var check = await JSPLib.storage.checkLocalDB(key, max_expires);
if (!(check)) {
printer.debuglog(`Network (${period_name} ${CU.counttype})`);
let data = await JSPLib.danbooru.getPostsCountdown(BuildTagParams(period, `${CU.usertag}:${username}`), max_post_limit_query, post_fields, domname);
let mapped_data = MapPostData(data);
if (limited) {
let indexed_posts = AssignPostIndexes(period, mapped_data, 0);
mapped_data = JSPLib.utility.mergeHashes(...tooltip_metrics.map((metric) => ({[metric]: GetAllStatistics(mapped_data, metric)})));
mapped_data.chart_data = JSPLib.utility.mergeHashes(...chart_metrics.map((metric) => ({[metric]: GetPeriodAverages(indexed_posts, metric)})));
mapped_data.chart_data.uploads = GetPeriodPosts(indexed_posts);
JSPLib.storage.saveData(key, {value: mapped_data, expires: JSPLib.utility.getExpires(max_expires)});
} else {
JSPLib.storage.saveData(key, {value: PreCompressData(mapped_data), expires: JSPLib.utility.getExpires(max_expires)});
}
return mapped_data;
}
return (limited ? check.value : PostDecompressData(check.value));
}
//Event handlers
async function GetPeriod(event) {
let header = event.target.parentElement;
let is_limited = $(event.target).hasClass("cu-limited");
let period = header.dataset.period;
$(`#count-header th[data-period=${period}] .cu-display`).show();
await GetPeriodUploads(CU.current_username, period, is_limited, `#count-header th[data-period=${period}] .cu-counter`);
CU.period_available[CU.usertag][CU.current_username][period] = true;
let column = header.cellIndex;
let $cells = $(`#count-table td:nth-of-type(${column + 1})`);
if (is_limited) {
let value = $(".cu-uploads", $cells[0]).html();
$($cells[0]).html(RenderTooltipData(value, period, true));
} else {
$cells.each((i, cell) => {
let value = $(".cu-uploads", cell).html();
$(cell).html(RenderTooltipData(value, period));
});
$(`.cu-select-tooltip[data-type="${CU.current_metric}"] a`).click();
}
$(`#count-header th[data-period=${period}] .cu-display`).hide();
$(`.cu-select-tooltip[data-type="${CU.current_metric}"] a`).click();
$(event.target).off(JSPLib.program_click);
}
function SortTable(event) {
if (event.target.tagName !== "TH") {
return;
}
let column = event.target.cellIndex + 1;
let period = $(`#count-header th:nth-of-type(${column})`).data('period');
if (CU.sortperiod !== period) {
CU.sorttype = 3;
CU.sortperiod = period;
}
let rows = [];
$("#count-table tr").each((i, row) => {
if (i === 0) {
return;
}
let data = $(`td:nth-of-type(${column}) .cu-uploads`, row).html();
let posts = data.match(/\((\d+)\/(\d+)\)/).slice(1, 3).map(Number);
rows.push({
domobj: $(row).detach(),
posts
});
});
rows.sort((a, b) => {
switch (CU.sorttype) {
case 1:
return b.posts[1] - a.posts[1];
case 2:
return a.posts[1] - b.posts[1];
case 3:
return b.posts[0] - a.posts[0];
case 0:
default:
return a.posts[0] - b.posts[0];
}
}).forEach((row) => {
$("#count-table tbody").append(row.domobj);
});
CU.sorttype = (CU.sorttype + 1) % 4;
$("#count-order").html(RenderOrderMessage(period, CU.sorttype));
}
function RenderChart(event) {
if (event.target.tagName !== "TD") {
return;
}
if (!chart_metrics.includes(CU.current_metric)) {
JSPLib.notice.notice("Chart data not available on Day and Week metrics.");
return;
}
let period = $(event.target).data('period');
let is_limited = $(event.target).hasClass("cu-limited");
let longname = period_info.longname[period];
let points = period_info.points[period];
let period_key = GetPeriodKey(longname);
let data = JSPLib.storage.getIndexedSessionData(period_key);
if (!data || (!is_limited && data.value.length === 0) || (is_limited && !data.value.chart_data)) {
JSPLib.notice.notice(`${period_info.header[period]} period not populated! Click the period header to activate the chart.`);
return;
}
var period_averages, period_uploads;
if (!is_limited) {
let time_offset = Date.now() - (data.expires - period_info.uploadexpires[period]);
let posts = PostDecompressData(data.value);
let indexed_posts = AssignPostIndexes(period, posts, time_offset);
period_averages = GetPeriodAverages(indexed_posts, CU.current_metric);
period_uploads = GetPeriodPosts(indexed_posts);
} else {
period_averages = data.value.chart_data[CU.current_metric];
period_uploads = data.value.chart_data.uploads;
}
let metric_display = JSPLib.utility.displayCase(CU.current_metric);
let type_display = JSPLib.utility.displayCase(CU.counttype);
let chart_data = {
title: {
text: `${JSPLib.utility.displayCase(longname)} ${CU.counttype} - Average post ${CU.current_metric}`
},
axisX: {
title: period_info.xlabel[period],
minimum: 0,
maximum: (points ? points - 1 : period_uploads.slice(-1)[0].x)
},
axisY: {
title: `${metric_display}`
},
axisY2: {
title: `${type_display}`,
},
legend: {
horizontalAlign: "right",
verticalAlign: "bottom",
},
data: [{
showInLegend: true,
legendText: `${metric_display}`,
type: "spline",
dataPoints: period_averages
},
{
showInLegend: true,
legendText: `${type_display}`,
type: "line",
axisYType: "secondary",
dataPoints: period_uploads
}]
};
//This needs to be shown now so that the chart function renders to the right size
$("#count-chart").show();
var chart = new CanvasJS.Chart("count-chart", chart_data);
chart.render();
$(".canvasjs-chart-credit").css('top', "400px");
}
function TooltipChange(event) {
CU.current_metric = $(event.target.parentElement).data('type');
$(".cu-select-tooltip,.cu-tooltiptext").removeClass("cu-activetooltip");
$(`.cu-select-tooltip[data-type="${CU.current_metric}"]`).addClass("cu-activetooltip");
$(`.cu-tooltiptext[data-type="${CU.current_metric}"]`).addClass("cu-activetooltip");
JSPLib.storage.setLocalData('cu-current-metric', CU.current_metric);
$(".cu-hover .cu-uploads").off(JSPLib.program_mouseover).on(JSPLib.program_mouseover, TooltipHover);
event.preventDefault();
}
function ToggleCopyrightsSection(event) {
$(event.target).closest('a').toggleClass("cu-triangle-right cu-triangle-down");
$('#count-copyrights-section').slideToggle(100);
}
function ToggleCopyrightTag(event) {
let $container = $(event.target.parentElement);
$container.toggleClass("cu-active-copyright");
let copyright = $container.data('copyright');
if ($container.hasClass("cu-active-copyright")) {
CU.active_copytags.push(copyright);
} else {
CU.active_copytags.splice(CU.active_copytags.indexOf(copyright), 1);
}
event.preventDefault();
}
async function CopyrightPeriod(event) {
let $container = $(event.target.parentElement);
let short_period = CU.copyright_period = $container.data('type');
$(".cu-select-period").removeClass("cu-active-period");
$container.addClass("cu-active-period");
if (short_period === 'manual') {
$("#count-copyrights-manual").show();
$('#count-copyrights-list').html(RenderCopyrights('manual'));
$("#count-copyrights-list a").off(JSPLib.program_click).on(JSPLib.program_click, ToggleCopyrightTag);
} else {
$("#count-copyrights-manual").hide();
let current_period = period_info.longname[short_period];
let is_period_enabled = CU.period_available[CU.usertag][CU.current_username][short_period];
if (is_period_enabled) {
if (CU.user_copytags[CU.usertag][CU.current_username][current_period] === undefined) {
let period_key = GetPeriodKey(current_period);
let data = JSPLib.storage.getIndexedSessionData(period_key);
let copyright_count = GetCopyrightCount(PostDecompressData(data.value));
let user_copytags = SortDict(copyright_count);
if (CU.user_settings.copyrights_merge) {
$("#count-copyrights-counter").html(copyright_counter);
user_copytags = await MergeCopyrightTags(user_copytags);
$("#count-copyrights-counter").html('');
}
CU.user_copytags[CU.usertag][CU.current_username][current_period] = user_copytags;
}
if (CU.user_copytags[CU.usertag][CU.current_username][current_period].length === 0) {
$('#count-copyrights-list').html(`${copyright_no_uploads}
`);
} else {
$('#count-copyrights-list').html(RenderCopyrights(current_period));
$("#count-copyrights-list a").off(JSPLib.program_click).on(JSPLib.program_click, ToggleCopyrightTag);
}
} else {
$('#count-copyrights-list').html(`${copyright_no_statistics}
`);
}
}
event.preventDefault();
}
function ToggleNotice(event) {
if (CU.hidden === true) {
CU.hidden = false;
$('#upload-counts').addClass('opened');
if (!PopulateTable.is_started) {
//Always show current user on open to prevent processing potentially bad usernames set by CheckUser
CU.empty_uploads_message = (CU.username === "Anonymous" ? empty_uploads_message_anonymous : empty_uploads_message_owner);
CU.display_username = CU.username;
CU.current_username = CU.username.toLowerCase();
CU.level_string = (CU.username === "Anonymous" ? 'Member' : Danbooru.CurrentUser.data('level-string'));
CU.usertag = 'user';
PopulateTable();
}
CU.channel.postMessage({type: "show"});
} else {
CU.hidden = true;
$('#upload-counts').removeClass('opened');
$('.cu-program-checkbox').prop('checked', false);
$("#count-chart").hide();
CU.channel.postMessage({type: "hide"});
}
JSPLib.storage.setLocalData('cu-hide-current-uploads', CU.hidden);
event.preventDefault();
}
function StashNotice(event) {
if (CU.stashed === true) {
CU.stashed = false;
$('#upload-counts,#upload-counts-restore').removeClass('stashed');
CU.channel.postMessage({type: "unstash"});
} else {
CU.stashed = true;
CU.hidden = true;
$('#upload-counts,#upload-counts-restore').removeClass('opened').addClass('stashed');
$('.cu-program-checkbox').prop('checked', false);
$("#count-chart").hide();
CU.channel.postMessage({type: "stash"});
}
JSPLib.storage.setLocalData('cu-stash-current-uploads', CU.stashed);
JSPLib.storage.setLocalData('cu-hide-current-uploads', CU.hidden);
event.preventDefault();
}
async function RefreshUser() {
$("#count-copyrights-counter").html(copyright_counter);
let diff_tags = JSPLib.utility.arrayDifference(CU.active_copytags, CU.shown_copytags);
let promise_array = [];
diff_tags.forEach((val) => {
promise_array.push(GetTagData(`${CU.usertag}:${CU.current_username} ${val}`));
promise_array.push(GetTagData(val));
});
await Promise.all(promise_array);
$("#count-copyrights-counter").html('');
InitializeTable();
}
async function CheckUser() {
//Don't change the username while currently processing
if (!PopulateTable.is_started) {
$("#count-chart").hide();
let check_user;
let check_username = $("#count_query_user_id").val().toLowerCase();
if (check_username === "") {
check_user = [];
} else if (check_username in CU.checked_usernames) {
check_user = CU.checked_usernames[check_username];
} else {
//Check each time no matter what as misses can be catastrophic
check_user = await JSPLib.danbooru.submitRequest('users', {search: {name_matches: check_username}, only: user_fields, expiry: 30});
CU.checked_usernames[check_username] = check_user;
}
if (check_user.length) {
CU.display_username = check_user[0].name;
CU.current_username = check_user[0].name.toLowerCase();
CU.level_string = check_user[0].level_string;
let is_approvals = $("#count_approver_select")[0].checked;
CU.empty_uploads_message = is_approvals ? empty_approvals_message_other : empty_uploads_message_other;
CU.usertag = is_approvals ? 'approver' : 'user';
CU.counttype = is_approvals ? 'approvals' : 'uploads';
PopulateTable();
} else {
TableMessage(`User doesn't exist!
`);
}
}
}
async function AddCopyright() {
let user_copytags = CU.user_copytags[CU.usertag][CU.current_username];
let tag = $("#count_query_copyright").val();
let tagdata = await JSPLib.danbooru.submitRequest('tags', {search: {name: tag}, only: name_field}, {default_val: []});
if (tagdata.length === 0) {
JSPLib.notice.notice('Tag not valid');
return;
}
tag = tagdata[0].name;
user_copytags.manual.push(tag);
user_copytags.manual = JSPLib.utility.arrayUnique(user_copytags.manual);
CU.active_copytags.push(tag);
CU.active_copytags = JSPLib.utility.arrayUnique(CU.active_copytags);
$('#count-copyrights-list').html(RenderCopyrights('manual'));
$("#count-copyrights-list a").off(JSPLib.program_click).on(JSPLib.program_click, ToggleCopyrightTag);
}
function TooltipHover(event) {
let container = event.target.parentElement;
let $tooltip_text = $(".cu-activetooltip", container);
let tooltip_key = $(container.parentElement.parentElement).data('key');
let tooltip_period = $(container).data('period');
let tooltip_metric = $(".cu-activetooltip", container).data('type');
$tooltip_text.html("Loading!");
$tooltip_text.html(RenderStatistics(tooltip_key, tooltip_metric, tooltip_period));
$(event.target).off();
}
//Main execution functions
async function ProcessUploads() {
var promise_array = [];
var current_uploads = [];
var user_copytags = [];
if (CU.current_username !== "Anonymous") {
current_uploads = await GetPeriodUploads(CU.current_username, 'd');
}
let previous_key = GetPeriodKey("previous");
if (current_uploads.length) {
let is_new_tab = JSPLib.storage.getIndexedSessionData(previous_key) === null;
let previous_uploads = await JSPLib.storage.checkLocalDB(previous_key, {default_val: []});
previous_uploads = PostDecompressData(previous_uploads.value);
let current_ids = JSPLib.utility.getObjectAttributes(current_uploads, 'id');
let previous_ids = JSPLib.utility.getObjectAttributes(previous_uploads, 'id');
if (is_new_tab || !JSPLib.utility.arrayEquals(current_ids, previous_ids) || IsMissingTag(`${CU.usertag}:${CU.current_username}`)) {
promise_array.push(GetTagData(`${CU.usertag}:${CU.current_username}`));
}
if (CU.is_gold_user && CU.user_settings.copyrights_enabled) {
let curr_copyright_count = GetCopyrightCount(current_uploads);
let prev_copyright_count = GetCopyrightCount(previous_uploads);
user_copytags = SortDict(curr_copyright_count);
if (CU.user_settings.copyrights_merge) {
user_copytags = await MergeCopyrightTags(user_copytags);
}
if (CU.user_settings.copyrights_threshold) {
user_copytags = user_copytags.slice(0, CU.user_settings.copyrights_threshold);
}
let copyright_symdiff = CompareCopyrightCounts(curr_copyright_count, prev_copyright_count);
let copyright_changed = (is_new_tab ? user_copytags : JSPLib.utility.arrayIntersection(user_copytags, copyright_symdiff));
let copyright_nochange = (is_new_tab ? [] : JSPLib.utility.arrayDifference(user_copytags, copyright_changed));
copyright_nochange.forEach((val) => {
if (CheckCopyrightVelocity(val) || IsMissingTag(val)) {
promise_array.push(GetTagData(val));
}
if (IsMissingTag(`${CU.usertag}:${CU.current_username} ${val}`)) {
promise_array.push(GetTagData(`${CU.usertag}:${CU.current_username} ${val}`));
}
});
copyright_changed.forEach((val) => {
promise_array.push(GetTagData(`${CU.usertag}:${CU.current_username} ${val}`));
promise_array.push(GetTagData(val));
});
}
await Promise.all(promise_array);
} else if (IsMissingTag(`${CU.usertag}:${CU.current_username}`)) {
await GetTagData(`${CU.usertag}:${CU.current_username}`);
}
CU.user_copytags[CU.usertag][CU.current_username] = {daily: user_copytags, manual: []};
JSPLib.storage.saveData(previous_key, {value: PreCompressData(current_uploads), expires: 0});
return current_uploads;
}
function CleanupTasks() {
JSPLib.storage.pruneProgramCache(PROGRAM_DATA_REGEX, prune_expires);
}
//Cache functions
function OptionCacheDataKey(data_type, data_value) {
CU.data_period = $("#cu-control-data-period").val();
if (data_type === "reverse_implication") {
return 'rti-' + data_value;
}
if (data_type === "count") {
if (CU.data_period === "previous") {
CU.data_value = "";
return "";
}
let shortkey = (CU.data_period !== "" ? longname_key[CU.data_period] : "");
return `ct${shortkey}-${data_value}`;
}
return `${CU.data_period}-${data_type}-${data_value}`;
}
//Settings functions
function BroadcastCU(ev) {
let printer = JSPLib.debug.getFunctionPrint('BroadcastCU');
printer.debuglog(`(${ev.data.type}):`, ev.data);
switch (ev.data.type) {
case "hide":
CU.hidden = true;
$('#upload-counts').removeClass('opened');
break;
case "show":
CU.hidden = false;
$('#upload-counts').addClass('opened');
break;
case "stash":
CU.stashed = true;
CU.hidden = true;
$('#upload-counts,#upload-counts-restore').removeClass('opened').addClass('stashed');
break;
case "unstash":
CU.stashed = false;
$('#upload-counts,#upload-counts-restore').removeClass('stashed');
//falls through
default:
//do nothing
}
}
function RemoteResetCallback() {
if (!CU.hidden && !CU.stashed) {
CU.hidden = true;
$('#upload-counts').removeClass('opened');
}
}
function GetShownPeriodKeys() {
return timevalues.filter((period_key) => CU.user_settings.periods_shown.includes(period_info.longname[period_key]));
}
function DataTypeChange() {
let data_type = $('#cu-control-data-type').val();
let action = (['count', 'uploads', 'approvals'].includes(data_type) ? 'show' : 'hide');
$('.cu-options[data-setting="data_period"]')[action]();
}
function InitializeProgramValues() {
Object.assign(CU, {
username: Danbooru.CurrentUser.data('name'),
is_gold_user: Danbooru.CurrentUser.data('is-gold'),
current_metric: JSPLib.storage.checkLocalData('cu-current-metric', {default_val: 'score'}),
hidden: Boolean(JSPLib.storage.checkLocalData('cu-hide-current-uploads', {default_val: true})),
stashed: Boolean(JSPLib.storage.checkLocalData('cu-stash-current-uploads', {default_val: false})),
});
JSPLib.load.setProgramGetter(CU, 'IAC', 'IndexedAutocomplete', 29.25);
return true;
}
function RenderSettingsMenu() {
$("#current-uploads").append(JSPLib.menu.renderMenuFramework(MENU_CONFIG));
$("#cu-general-settings").append(JSPLib.menu.renderDomainSelectors());
$("#cu-display-settings").append(JSPLib.menu.renderCheckbox('copyrights_merge'));
$("#cu-display-settings").append(JSPLib.menu.renderCheckbox('copyrights_enabled'));
$("#cu-display-settings").append(JSPLib.menu.renderTextinput('copyrights_threshold', 10));
$("#cu-display-settings").append(JSPLib.menu.renderTextinput('postcount_threshold', 10));
$("#cu-display-settings").append(JSPLib.menu.renderInputSelectors('periods_shown', 'checkbox'));
$('#cu-controls').append(JSPLib.menu.renderCacheControls());
$('#cu-cache-controls-message').append(JSPLib.menu.renderExpandable("Cache Data details", CACHE_DATA_DETAILS));
$("#cu-cache-controls").append(JSPLib.menu.renderLinkclick('cache_info', true));
$('#cu-cache-controls').append(JSPLib.menu.renderCacheInfoTable());
$("#cu-cache-controls").append(JSPLib.menu.renderLinkclick('purge_cache', true));
$('#cu-controls').append(JSPLib.menu.renderCacheEditor(true));
$('#cu-cache-editor-message').append(JSPLib.menu.renderExpandable("Program Data details", PROGRAM_DATA_DETAILS));
$("#cu-cache-editor-controls").append(JSPLib.menu.renderKeyselect('data_source', true));
$("#cu-cache-editor-controls").append(JSPLib.menu.renderDataSourceSections());
$("#cu-section-indexed-db").append(JSPLib.menu.renderKeyselect('data_type', true));
$("#cu-section-indexed-db").append(JSPLib.menu.renderKeyselect('data_period', true));
$("#cu-section-local-storage").append(JSPLib.menu.renderCheckbox('raw_data', true));
$("#cu-cache-editor-controls").append(JSPLib.menu.renderTextinput('data_name', 20, true));
JSPLib.menu.engageUI(true);
$("#cu-select-periods-shown-daily").checkboxradio("disable"); //Daily period is mandatory
JSPLib.menu.saveUserSettingsClick();
JSPLib.menu.resetUserSettingsClick(LOCALSTORAGE_KEYS, RemoteResetCallback);
JSPLib.menu.cacheInfoClick();
JSPLib.menu.purgeCacheClick();
JSPLib.menu.expandableClick();
JSPLib.menu.dataSourceChange();
$("#cu-control-data-type").on(JSPLib.program_change, 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();
}
//Main program
function Main() {
const preload = {
run_on_settings: true,
default_data: DEFAULT_VALUES,
reset_data: PROGRAM_RESET_KEYS,
initialize_func: InitializeProgramValues,
broadcast_func: BroadcastCU,
render_menu_func: RenderSettingsMenu,
program_css: PROGRAM_CSS,
};
if (!JSPLib.menu.preloadScript(CU, preload)) return;
var $notice_box = $(notice_box);
var $footer_notice = $(unstash_notice);
if (CU.stashed === true) {
$notice_box.addClass('stashed');
$footer_notice.addClass('stashed');
//The table needs to be hidden when it's stashed
CU.hidden = true;
}
$('header#top').append($notice_box);
$('footer#page-footer').append($footer_notice);
$("#toggle-count-notice").on(JSPLib.program_click, ToggleNotice);
$("#stash-count-notice,#restore-count-notice").on(JSPLib.program_click, StashNotice);
if (CU.hidden === false) {
//Set to opposite so that click can be used and sets it back
CU.hidden = true;
setTimeout(() => {$("#toggle-count-notice").click();}, JQUERY_DELAY);
}
JSPLib.statistics.addPageStatistics();
JSPLib.load.noncriticalTasks(CleanupTasks);
}
/****Initialization****/
//Variables for JSPLib
JSPLib.program_name = PROGRAM_NAME;
JSPLib.program_shortcut = PROGRAM_SHORTCUT;
JSPLib.program_data = CU;
//Variables for debug.js
JSPLib.debug.mode = false;
JSPLib.debug.level = JSPLib.debug.INFO;
//Variables for menu.js
JSPLib.menu.program_reset_data = PROGRAM_RESET_KEYS;
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 network.js
JSPLib.network.counter_domname = "#loading-counter";
//Export JSPLib
JSPLib.load.exportData();
//Variables for storage.js
JSPLib.storage.indexedDBValidator = ValidateEntry;
JSPLib.storage.localSessionValidator = ValidateProgramData;
/****Execution start****/
JSPLib.load.programInitialize(Main, {required_variables: PROGRAM_LOAD_REQUIRED_VARIABLES, required_selectors: PROGRAM_LOAD_REQUIRED_SELECTORS});