· 6 years ago · May 04, 2019, 10:58 AM
1// ==UserScript==
2// @name better_better_booru
3// @namespace https://greasyfork.org/scripts/3575-better-better-booru
4// @author otani, modified by Jawertae, A Pseudonymous Coder & Moebius Strip.
5// @description Several changes to make Danbooru much better.
6// @version 8.2.3
7// @match *://*.donmai.us/*
8// @run-at document-end
9// @grant GM_setValue
10// @grant GM_getValue
11// @grant GM_deleteValue
12// @grant GM_listValues
13// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAAA9klEQVQ4y2NgGBgQu/Dau1/Pt/rhVPAfCkpwKXhUZ8Al2vT//yu89vDjV8AkP/P//zY0K//+eHVmoi5YyB7I/VDGiKYADP60wRT8P6aKTcH//0lgQcHS//+PYFdwFu7Ib8gKGBgYOQ22glhfGO7mqbEpzv///xyqAiAQAbGewIz8aoehQArEWsyQsu7O549XJiowoCpg4rM9CGS8V8UZ9GBwy5wBr4K/teL4Ffz//8mHgIL/v82wKgA6kkXE+zKIuRaHAhDQATFf4lHABmL+xKPAFhKUOBQwSyU+AzFXEvDFf3sCCnrxh8O3Ujwh+fXZvjoZ+udTAERqR5IgKEBRAAAAAElFTkSuQmCC
14// ==/UserScript==
15
16// Have a nice day. - A Pseudonymous Coder
17
18function bbbScript() { // Wrapper for injecting the script into the document.
19 /*
20 * NOTE: You no longer need to edit this script to change settings!
21 * Use the "BBB Settings" button in the menu instead.
22 */
23
24 // If Danbooru's JS isn't available, assume we're somewhere this script isn't needed and stop.
25 if (typeof(Danbooru) === "undefined")
26 return;
27
28 /* Helper Prototypes */
29 // Don't get hoisted so they should be declared at the top to simplify things.
30 String.prototype.bbbSpacePad = function() {
31 // Add a leading and trailing space.
32 return (this.length ? " " + this + " " : "");
33 };
34
35 String.prototype.bbbSpaceClean = function() {
36 // Remove leading, trailing, and multiple spaces.
37 return this.replace(/\s+/g, " ").replace(/^\s|\s$/g, "");
38 };
39
40 String.prototype.bbbTagClean = function() {
41 // Remove extra commas along with leading, trailing, and multiple spaces.
42 return this.replace(/(?:^|[\s,]+)(%?\))(?:$|\s+)/, " $1 ").replace(/(?:^|\s+)([~-]*\(%?)(?:$|[\s,]+)/g, " $1 ").replace(/[\s,]*,[\s,]*/g, ", ").replace(/[\s,]+$|^[\s,]+/g, "").replace(/\s+/g, " ");
43 };
44
45 String.prototype.bbbHash = function() {
46 // Turn a string into a hash using the current Danbooru hash method.
47 var hash = 5381;
48 var i = this.length;
49
50 while(i)
51 hash = (hash * 33) ^ this.charCodeAt(--i);
52
53 return hash >>> 0;
54 };
55
56 Number.prototype.bbbEncode62 = function() {
57 // Encode a number to base62.
58 var encodeChars = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];
59 var encoded = "";
60 var num = this;
61
62 if (num === 0)
63 encoded = encodeChars[0];
64 else {
65 while (num > 0) {
66 encoded = encodeChars[num % 62] + encoded;
67 num = Math.floor(num / 62);
68 }
69 }
70
71 return encoded;
72 };
73
74 Element.prototype.bbbParent = function(parentName, limit) {
75 // Search an element's successive parent nodes for a specific tag name or until the amount limit is hit.
76 var el = this;
77 var search = parentName.toUpperCase();
78
79 for (var i = 0, il = limit || 1; i < il; i++) {
80 var parent = el.parentNode;
81
82 if (parent && parent.tagName === search)
83 return parent;
84 else
85 el = parent;
86 }
87
88 return null;
89 };
90
91 Element.prototype.bbbGetPadding = function() {
92 // Get all the padding measurements of an element including the total width and height.
93 if (window.getComputedStyle) {
94 var computed = window.getComputedStyle(this, null);
95 var paddingLeft = parseFloat(computed.paddingLeft);
96 var paddingRight = parseFloat(computed.paddingRight);
97 var paddingTop = parseFloat(computed.paddingTop);
98 var paddingBottom = parseFloat(computed.paddingBottom);
99 var paddingHeight = paddingTop + paddingBottom;
100 var paddingWidth = paddingLeft + paddingRight;
101 return {width: paddingWidth, height: paddingHeight, top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight};
102 }
103 };
104
105 Element.prototype.bbbHasClass = function() {
106 // Test an element for one or more collections of classes.
107 var classList = this.classList;
108
109 for (var i = 0, il = arguments.length; i < il; i++) {
110 var classes = arguments[i].bbbSpaceClean();
111
112 if (!classes)
113 continue;
114
115 var classArray = classes.split(" ");
116 var hasClass = true;
117
118 for (var j = 0, jl = classArray.length; j < jl; j++) {
119 if (!classList.contains(classArray[j])) {
120 hasClass = false;
121 break;
122 }
123 }
124
125 if (hasClass)
126 return true;
127 }
128
129 return false;
130 };
131
132 Element.prototype.bbbAddClass = function(classString) {
133 // Add one or more classes to an element.
134 var classes = classString.bbbSpaceClean();
135
136 if (!classes)
137 return;
138
139 var classList = this.classList;
140 var classArray = classes.split(" ");
141
142 for (var i = 0, il = classArray.length; i < il; i++)
143 classList.add(classArray[i]);
144 };
145
146 Element.prototype.bbbRemoveClass = function(classString) {
147 // Remove one or more classes from an element.
148 var classes = classString.bbbSpaceClean();
149
150 if (!classes)
151 return;
152
153 var classList = this.classList;
154 var classArray = classes.split(" ");
155
156 for (var i = 0, il = classArray.length; i < il; i++)
157 classList.remove(classArray[i]);
158 };
159
160 Element.prototype.bbbWatchNodes = function(func) {
161 // Watch for new nodes.
162 var observer = window.MutationObserver || window.WebKitMutationObserver;
163
164 if (observer) {
165 observer = new observer(func);
166 observer.observe(this, {childList: true, subtree: true});
167 }
168 else
169 this.addEventListener("DOMNodeInserted", func, false);
170 };
171
172 Element.prototype.bbbOverrideClick = function(func) {
173 // Override Danbooru's click event listeners by capturing clicks on the parent node and stopping them.
174 var target = this;
175
176 var wrapperFunc = function(event) {
177 if (event.target !== target || event.button !== 0)
178 return;
179
180 func(event);
181 event.stopPropagation();
182 };
183
184 target.parentNode.addEventListener("click", wrapperFunc, true);
185 };
186
187 Element.prototype.bbbInfo = function(name, value) {
188 // Retrieve or set info in data attributes.
189 if (this.tagName === "HTML") { // Pseudo document.bbbInfo for retrieved pages.
190 var imgContainer = getId("image-container", this);
191
192 if (typeof(value) !== "undefined" && imgContainer)
193 imgContainer.setAttribute("data-" + name, value);
194 else if (name && imgContainer)
195 return imgContainer.getAttribute("data-" + name);
196 else
197 return scrapePost(this);
198 }
199 else if (typeof(value) !== "undefined")
200 this.setAttribute("data-" + name, value);
201 else if (name)
202 return this.getAttribute("data-" + name);
203 else if (this.bbbHasClass("post-preview"))
204 return scrapeThumb(this);
205 else {
206 // Always try to send the HTML element in order to provide the most information.
207 var parent = this;
208
209 while (parent.parentNode)
210 parent = parent.parentNode;
211
212 return scrapePost(parent);
213 }
214 };
215
216 document.bbbInfo = function(name, value) {
217 // Document specific bbbInfo that goes along with the element prototype method.
218 var imgContainer = document.getElementById("image-container");
219
220 if (typeof(value) !== "undefined" && imgContainer)
221 imgContainer.setAttribute("data-" + name, value);
222 else if (name && imgContainer)
223 return imgContainer.getAttribute("data-" + name);
224 else
225 return scrapePost(document);
226 };
227
228 Storage.prototype.bbbSetItem = function(key, value) {
229 // Store a value in storage and warn if it is full.
230 try {
231 this.setItem(key, value);
232 }
233 catch (error) {
234 if (error.code === 22 || error.code === 1014) {
235 if (this === localStorage) {
236 if (!bbb.flags.local_storage_full) {
237 if (localStorage.length > 2000) {
238 // Try clearing out autocomplete if that appears to be the problem.
239 cleanLocalStorage("autocomplete");
240
241 try {
242 localStorage.setItem(key, value);
243 }
244 catch (localError) {
245 bbb.flags.local_storage_full = true;
246 }
247 }
248 else
249 bbb.flags.local_storage_full = true;
250
251 // Store the local storage value until it can be retried.
252 if (bbb.flags.local_storage_full) {
253 bbb.local_storage_queue = {};
254 bbb.local_storage_queue[key] = value;
255 localStorageDialog();
256 }
257 }
258 else {
259 // Temporarily store additional local storage values until they can be retried.
260 if (sessionStorage.getItem("bbb_local_storage_queue")) {
261 var sessLocal = parseJson(sessionStorage.getItem("bbb_local_storage_queue"), {});
262
263 sessLocal[key] = value;
264 sessionStorage.bbbSetItem("bbb_local_storage_queue", JSON.stringify(sessLocal));
265 }
266 else
267 bbb.local_storage_queue[key] = value;
268 }
269 }
270 else {
271 // Keep only a few values in session storage.
272 for (var i = sessionStorage.length - 1; i >= 0; i--) {
273 var keyName = sessionStorage.key(i);
274
275 if (keyName !== "bbb_endless_default" && keyName !== "bbb_quick_search" && keyName !== "bbb_posts_cache")
276 sessionStorage.removeItem(keyName);
277 }
278
279 try {
280 sessionStorage.setItem(key, value);
281 }
282 catch (sessionError) {
283 bbbNotice("Your settings/data could not be saved/updated. The browser's session storage is full.", -1);
284 }
285 }
286 }
287 else
288 bbbNotice("Unexpected error while attempting to save/update settings. (Error: " + error.message + ")", -1);
289 }
290 };
291
292 /* Global Variables */
293 var bbb = { // Container for script info.
294 autocomplete: {},
295 blacklist: {
296 entries: [],
297 match_list: {},
298 smart_view_target: undefined
299 },
300 custom_tag: {
301 searches: [],
302 style_list: {}
303 },
304 dialog: {
305 queue: []
306 },
307 drag_scroll: {
308 lastX: undefined,
309 lastY: undefined,
310 moved: false,
311 target: undefined
312 },
313 el: { // Script elements.
314 menu: {} // Menu elements.
315 },
316 endless: {
317 append_page: false,
318 enabled: false,
319 fill_first_page: false,
320 last_paginator: undefined,
321 new_paginator: undefined,
322 no_thumb_count: 0,
323 pages: [],
324 paused: false,
325 posts: {}
326 },
327 fixed_paginator_space: 0,
328 fixed_sidebar: {
329 content: undefined,
330 left: undefined,
331 sidebar: undefined,
332 top: undefined
333 },
334 flags: {},
335 gm_data: undefined,
336 groups: undefined,
337 hotkeys: {
338 other: { // Hotkeys for misc locations.
339 66: {func: openMenu} // B
340 },
341 post: { // Post hotkeys.
342 49: {func: resizeHotkey, custom_handler: true}, // 1
343 50: {func: resizeHotkey, custom_handler: true}, // 2
344 51: {func: resizeHotkey, custom_handler: true}, // 3
345 52: {func: resizeHotkey, custom_handler: true}, // 4
346 66: {func: openMenu}, // B
347 86: {func: swapPost} // V
348 }
349 },
350 post: { // Post content info and status.
351 info: {}, // Post information object.
352 resize: {
353 mode: "none",
354 ratio: 1
355 },
356 swapped: false // Whether the post content has been changed between the original and sample versions.
357 },
358 options: { // Setting options and data.
359 bbb_version: "8.2.3",
360 add_popular_link: newOption("checkbox", false, "Add Popular Link", "Add a link to the popular listing to the \"posts\" submenu"),
361 add_random_post_link: newOption("checkbox", false, "Add Random Link", "Add a link to a random post to the post sidebar options menu."),
362 alternate_image_swap: newOption("checkbox", false, "Alternate Image Swap", "Switch between the sample and original image by clicking the image. <tiphead>Note</tiphead>Notes can be toggled by using the link in the sidebar options section."),
363 autohide_sidebar: newOption("dropdown", "none", "Auto-hide Sidebar", "Hide the sidebar for posts, favorites listings, and/or searches until the mouse comes close to the left side of the window or the sidebar gains focus.<tiphead>Tips</tiphead>By using Danbooru's hotkey for the letter \"Q\" to place focus on the search box, you can unhide the sidebar.<br><br>Use the \"thumbnail count\" option to get the most out of this feature on search listings.", {txtOptions:["Disabled:none", "Favorites:favorites", "Posts:post", "Searches:search", "Favorites & Posts:favorites post", "Favorites & Searches:favorites search", "Posts & Searches:post search", "All:favorites post search"]}),
364 autoscroll_post: newOption("dropdown", "none", "Auto-scroll Post", "Automatically scroll a post to a particular point. <tipdesc>Below Header:</tipdesc> Scroll the window down until the header is no longer visible or scrolling is no longer possible. <tipdesc>Post Content:</tipdesc> Position the post content as close as possible to the left and top edges of the window viewport when initially loading a post. Using this option will also scroll past any notices above the content.", {txtOptions:["Disabled:none", "Below Header:header", "Post Content:post"]}),
365 blacklist_add_bars: newOption("checkbox", false, "Additional Bars", "Add a blacklist bar to the comment search listing and individually linked comments so that blacklist entries can be toggled as needed."),
366 blacklist_highlight_color: newOption("text", "#CCCCCC", "Highlight Color", "When using highlighting for \"thumbnail marking\", you may set the color here. <tiphead>Notes</tiphead>Leaving this field blank will result in the default color being used. <br><br>For easy color selection, use one of the many free tools on the internet like <a target=\"_blank\" href=\"http://www.quackit.com/css/css_color_codes.cfm\">this one</a>. Hex RGB color codes (#000000, #FFFFFF, etc.) are the recommended values."),
367 blacklist_ignore_fav: newOption("checkbox", false, "Ignore Favorites", "Allow the blacklist to ignore your favorited posts."),
368 blacklist_thumb_controls: newOption("checkbox", false, "Thumbnail Controls", "Allow control over individual blacklisted thumbnails and access to blacklist toggle links from blacklisted thumbnails. <tiphead>Directions</tiphead>For blacklisted thumbnails that have been revealed, hovering over them will reveal a clickable \"X\" icon that can hide them again. <br><br>If using \"hidden\" or \"replaced\" for the \"post display\" option, clicking on the area of a blacklisted thumbnail will pop up a menu that displays what blacklist entries it matches. Clicking the thumbnail area a second time while that menu is open will reveal that single thumbnail. <br><br>The menu that pops up on the first click also allows for toggling any listed blacklist entry for the entire page and navigating to the post without revealing its thumbnail. <tiphead>Note</tiphead>Toggling blacklist entries will have no effect on posts that have been changed via their individual controls."),
369 blacklist_post_display: newOption("dropdown", "disabled", "Post Display", "Set how the display of blacklisted posts in thumbnail listings and the comments section is handled. <tipdesc>Removed:</tipdesc> Posts and the space they take up are completely removed. <tipdesc>Hidden:</tipdesc> Post space is preserved, but thumbnails are hidden. <tipdesc>Replaced:</tipdesc> Thumbnails are replaced by \"blacklisted\" thumbnail placeholders.", {txtOptions:["Disabled:disabled", "Removed:removed", "Hidden:hidden", "Replaced:replaced"]}),
370 blacklist_smart_view: newOption("checkbox", false, "Smart View", "When navigating to a blacklisted post by using its thumbnail, if the thumbnail has already been revealed, the post content will temporarily be exempt from any blacklist checks for 1 minute and be immediately visible. <tiphead>Note</tiphead>Thumbnails in the parent/child notices of posts with exempt content will still be affected by the blacklist."),
371 blacklist_session_toggle: newOption("checkbox", false, "Session Toggle", "When toggling an individual blacklist entry on and off, the mode it's toggled to will persist across other pages in the same browsing session until it ends.<tiphead>Note</tiphead>For blacklists with many entries, this option can cause unexpected behavior (ex: getting logged out) if too many entries are toggled off at the same time."),
372 blacklist_thumb_mark: newOption("dropdown", "none", "Thumbnail Marking", "Mark the thumbnails of blacklisted posts that have been revealed to make them easier to distinguish from other thumbnails. <tipdesc>Highlight:</tipdesc> Change the background color of blacklisted thumbnails. <tipdesc>Icon Overlay:</tipdesc> Add an icon to the lower right corner of blacklisted thumbnails.", {txtOptions:["Disabled:none", "Highlight:highlight", "Icon Overlay:icon"]}),
373 blacklist_video_playback: newOption("dropdown", "pause resume", "Video Playback", "Allow the blacklist to control the playback of video content. <tipdesc>Pause:</tipdesc> Video content will pause when it is hidden. <tipdesc>Pause and Resume:</tipdesc> Video content will pause when it is hidden and resume playing when unhidden.", {txtOptions:["Disabled:disabled", "Pause:pause", "Pause & Resume:pause resume"]}),
374 border_spacing: newOption("dropdown", 0, "Border Spacing", "Set the amount of blank space between a border and thumbnail and between a custom tag border and status border. <tiphead>Note</tiphead>Even when set to 0, status borders and custom tag borders will always have a minimum value of 1 between them. <tiphead>Tip</tiphead>Use this option if you often have trouble distinguishing a border from the thumbnail image.", {txtOptions:["0 (Default):0", "1:1", "2:2", "3:3"]}),
375 border_width: newOption("dropdown", 2, "Border Width", "Set the width of thumbnail borders.", {txtOptions:["1:1", "2 (Default):2", "3:3", "4:4", "5:5"]}),
376 bypass_api: newOption("checkbox", false, "Automatic API Bypass", "When logged out and API only features are enabled, do not warn about needing to be logged in. Instead, automatically bypass those features."),
377 clean_links: newOption("checkbox", false, "Clean Links", "Remove the extra information after the post ID in thumbnail listing post links.<tiphead>Note</tiphead>Enabling this option will disable Danbooru's search navigation and active pool/favorite group detection for posts."),
378 collapse_sidebar: newOption("checkbox", false, "Collapsible Sidebar", "Allow sections in the sidebar to be expanded and collapsed via clicking their header titles.<tiphead>Note</tiphead>Sections can be set to default to expanded or collapsed by right clicking their titles."),
379 comment_score: newOption("checkbox", false, "Comment Scores", "Make comment scores visible by adding them as direct links to their respective comments."),
380 custom_status_borders: newOption("checkbox", false, "Custom Status Borders", "Override Danbooru's thumbnail borders for deleted, flagged, pending, parent, and child images."),
381 custom_tag_borders: newOption("checkbox", true, "Custom Tag Borders", "Add thumbnail borders to posts with specific tags."),
382 direct_downloads: newOption("checkbox", false, "Direct Downloads", "Allow download managers to download the posts displayed in the favorites, search, pool, popular, and favorite group listings. <tiphead>Note</tiphead>Posts filtered out by the blacklist or quick search will not provide direct downloads until the blacklist entry or quick search affecting them is disabled."),
383 disable_embedded_notes: newOption("checkbox", false, "Disable Embedded Notes", "Force posts with embedded notes to display with the original note styling. <tiphead>Notes</tiphead>While notes will display with the original styling, the actual post settings will still have embedded notes set to enabled. <br><br>Due to the actual settings, users that may wish to edit notes will have to edit the notes with the embedded note styling so that nothing ends up breaking in unexpected ways. When toggling translation mode or opening the edit note dialog box, the notes will automatically revert back to the original embedded notes until the page is reloaded. <br><br>Note resizing and moving will be allowed without the reversion to embedded notes since this ability is sometimes necessary for badly positioned notes. Any note resizing or moving done as a part of intended note editing should be done <b>after</b> triggering the embedded note reversion since any changes before it will be lost."),
384 disable_tagged_filenames: newOption("checkbox", false, "Disable Tagged Filenames", "Remove the tag information from post filenames and only leave the original md5 hash. <tiphead>Note</tiphead>For logged in users with their account's \"disable tagged filenames\" setting set to \"yes\", this option must be enabled for consistent behavior on hidden posts. <br><br>For logged in users with their account's \"disable tagged filenames\" setting set to \"no\" and logged out users, this option can be enabled to remove the tags from filenames without having to use an account setting."),
385 enable_menu_autocomplete: newOption("checkbox", true, "Enable Menu Autocomplete", "Allow the BBB menu to use Danbooru's tag autocomplete."),
386 enable_status_message: newOption("checkbox", true, "Enable Status Message", "When requesting information from Danbooru, display the request status in the lower right corner."),
387 endless_default: newOption("dropdown", "disabled", "Default", "Enable endless pages on the favorites, search, pool, and favorite group listings. <tipdesc>Off:</tipdesc> Start up with all features off. <tipdesc>On:</tipdesc> Start up with all features on.<tipdesc>Paused:</tipdesc> Start up with all features on, but do not append new pages until the \"load more\" button is clicked. <tiphead>Note</tiphead>When not set to disabled, endless pages can be toggled between off and on/paused by using the \"E\" hotkey or the \"endless\" link next to the \"listing\" link in the page submenu. <tiphead>Tip</tiphead>The \"new tab/window\" and \"fixed paginator\" options can provide additional customization for endless pages.", {txtOptions:["Disabled:disabled", "Off:off", "On:on", "Paused:paused"]}),
388 endless_fill: newOption("checkbox", false, "Fill Pages", "When appending pages with missing thumbnails caused by hidden posts or removed duplicate posts, retrieve thumbnails from the following pages and add them to the new page until the desired number of thumbnails is reached. <tiphead>Note</tiphead>If using page separators, the displayed page number for appended pages composed of thumbnails from multiple Danbooru pages will be replaced by a range consisting of the first and last pages from which thumbnails were retrieved."),
389 endless_pause_interval: newOption("dropdown", 0, "Pause Interval", "Pause endless pages each time the number of pages reaches a multiple of the selected amount. <tiphead>Note</tiphead> When this option is enabled, the \"Shift + E\" hotkey can be used to continue loading more pages after pausing.", {txtOptions:["Disabled:0"], numRange:[1,100]}),
390 endless_preload: newOption("checkbox", false, "Preload Next Page", "Start loading the next page as soon as possible.<tiphead>Note</tiphead>A preloaded page will not be appended until the scroll limit is reached."),
391 endless_remove_dup: newOption("checkbox", false, "Remove Duplicates", "When appending new pages, remove posts that already exist in the listing from the new page.<tiphead>Note</tiphead>Duplicate posts are caused by the addition of new posts to the beginning of a listing or changes to the order of the posts."),
392 endless_scroll_limit: newOption("dropdown", 500, "Scroll Limit", "Set the minimum amount of pixels that the window can have left to vertically scroll before it starts appending the next page.", {numList:[0,50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500]}),
393 endless_separator: newOption("dropdown", "divider", "Page Separator", "Distinguish pages from each other by marking them with a separator.<tipdesc>Marker:</tipdesc> Place a thumbnail sized marker before the first thumbnail of each page.<tipdesc>Divider:</tipdesc> Completely separate pages by placing a horizontal line between them.", {txtOptions:["None:none", "Marker:marker", "Divider:divider"]}),
394 endless_session_toggle: newOption("checkbox", false, "Session Toggle", "When toggling endless pages on and off, the mode it's toggled to will override the default and persist across other pages in the same browsing session for that tab until it ends."),
395 fixed_paginator: newOption("dropdown", "disabled", "Fixed Paginator", "Make the paginator always visible for the favorites, search, pool, and favorite group listings by fixing it to the bottom of the window when it would normally start scrolling out of view. <tipdesc>Endless:</tipdesc> Only change the paginator during endless pages browsing. <tipdesc>Normal:</tipdesc> Only change the paginator during normal browsing. <tipdesc>Always:</tipdesc> Change the paginator during normal and endless pages browsing. <tiphead>Note</tiphead>Options labeled with \"minimal\" will also make the fixed paginator smaller by removing most of the blank space within it.", {txtOptions:["Disabled:disabled", "Endless:endless", "Endless (Minimal):endless minimal", "Normal:normal", "Normal (Minimal):normal minimal", "Always:endless normal", "Always (Minimal):endless normal minimal"]}),
396 fixed_sidebar: newOption("dropdown", "none", "Fixed Sidebar", "Make the sidebar never completely vertically scroll out of view for posts, favorites listings, and/or searches by fixing it to the top or bottom of the window when it would normally start scrolling out of view. <tiphead>Note</tiphead>The \"auto-hide sidebar\" option will override this option if both try to modify the same page. <tiphead>Tip</tiphead>Depending on the available height in the browser window and the Danbooru location being modified, the \"tag scrollbars\", \"collapsible sidebar\", and/or \"remove tag headers\" options may be needed for best results.", {txtOptions:["Disabled:none", "Favorites:favorites", "Posts:post", "Searches:search", "Favorites & Posts:favorites post", "Favorites & Searches:favorites search", "Posts & Searches:post search", "All:favorites post search"]}),
397 hide_ban_notice: newOption("checkbox", false, "Hide Ban Notice", "Hide the Danbooru ban notice."),
398 hide_comment_notice: newOption("checkbox", false, "Hide Comment Guide Notice", "Hide the Danbooru comment guide notice."),
399 hide_fav_button: newOption ("checkbox", false, "Hide Favorite Button", "Hide the favorite button below post content."),
400 hide_pool_notice: newOption("checkbox", false, "Hide Pool Guide Notice", "Hide the Danbooru pool guide notice."),
401 hide_sign_up_notice: newOption("checkbox", false, "Hide Sign Up Notice", "Hide the Danbooru account sign up notice."),
402 hide_tag_notice: newOption("checkbox", false, "Hide Tag Guide Notice", "Hide the Danbooru tag guide notice."),
403 hide_tos_notice: newOption("checkbox", false, "Hide TOS Notice", "Hide the Danbooru terms of service agreement notice."),
404 hide_upgrade_notice: newOption("checkbox", false, "Hide Upgrade Notice", "Hide the Danbooru upgrade account notice."),
405 hide_upload_notice: newOption("checkbox", false, "Hide Upload Guide Notice", "Hide the Danbooru upload guide notice."),
406 hide_hidden_notice: newOption("checkbox", false, "Hide Hidden Posts Notice", "Hide the Danbooru hidden posts notice."),
407 image_swap_mode: newOption("dropdown", "load", "Image Swap Mode", "Set how swapping between the sample and original image is done.<tipdesc>Load First:</tipdesc> Display the image being swapped in after it has finished downloading. <tipdesc>View While Loading:</tipdesc> Immediately display the image being swapped in while it is downloading.", {txtOptions:["Load First:load", "View While Loading:view"]}),
408 load_sample_first: newOption("checkbox", true, "Load Sample First", "Load sample images first when viewing a post.<tiphead>Note</tiphead>When logged in, the account's \"default image width\" setting will override this option. This behavior can be changed with the \"override sample setting\" option under the preferences tab."),
409 manage_cookies: newOption("checkbox", false, "Manage Notice Cookies", "When using the \"hide upgrade notice\", \"hide sign up notice\", and/or \"hide TOS notice\" options, also create cookies to disable these notices at the server level.<tiphead>Tip</tiphead>Use this feature if the notices keep flashing on your screen before being removed."),
410 minimize_status_notices: newOption("checkbox", false, "Minimize Status Notices", "Hide the Danbooru deleted, banned, flagged, appealed, and pending notices. When you want to see a hidden notice, you can click the appropriate status link in the information section of the sidebar."),
411 override_blacklist: newOption("dropdown", "logged_out", "Override Blacklist", "Allow the \"blacklist\" setting to override the default blacklist for logged out users and/or account blacklist for logged in users. <tipdesc>Logged out:</tipdesc> Override the default blacklist for logged out users. <tipdesc>Always:</tipdesc> Override the default blacklist for logged out users and account blacklist for logged in users.", {txtOptions:["Disabled:disabled", "Logged out:logged_out", "Always:always"]}),
412 override_resize: newOption("checkbox", false, "Override Resize Setting", "Allow the \"resize post\" setting to override the account \"fit images to window\" setting when logged in."),
413 override_sample: newOption("checkbox", false, "Override Sample Setting", "Allow the \"load sample first\" setting to override the account \"default image width\" setting when logged in. <tiphead>Note</tiphead>When using this option, your Danbooru account settings should have \"default image width\" set to the corresponding value of the \"load sample first\" script setting. Not doing so will cause your browser to always download both the sample and original image. If you often change the \"load sample first\" setting, leaving your account to always load the sample/850px image first is your best option."),
414 page_counter: newOption("checkbox", false, "Page Counter", "Add a page counter and \"go to page #\" input field near the top of listing pages. <tiphead>Note</tiphead>The total number of pages will not be displayed if the pages are using the \"previous & next\" paging system or the total number of pages exceeds the maximum amount allowed by your user account level."),
415 post_drag_scroll: newOption("checkbox", false, "Post Drag Scrolling", "While holding down left click on a post's content, mouse movement can be used to scroll the whole page and reposition the content.<tiphead>Note</tiphead>This option is automatically disabled when translation mode is active."),
416 post_link_new_window: newOption("dropdown", "none", "New Tab/Window", "Force post links in the search, pool, popular, favorites, and favorite group listings to open in a new tab/window. <tipdesc>Endless:</tipdesc> Only use new tabs/windows during endless pages browsing. <tipdesc>Normal:</tipdesc> Only use new tabs/windows during normal browsing. <tipdesc>Always:</tipdesc> Use new tabs/windows during normal and endless pages browsing. <tiphead>Notes</tiphead>When this option is active, holding down the control and shift keys while clicking a post link will open the post in the current tab/window.<br><br>Whether the post opens in a new tab or a new window depends upon your browser configuration. <tiphead>Tip</tiphead>This option can be useful as a safeguard to keep accidental left clicks from disrupting endless pages.", {txtOptions:["Disabled:disabled", "Endless:endless", "Normal:normal", "Always:endless normal"]}),
417 post_resize: newOption("checkbox", true, "Resize Post", "Shrink large post content to fit the browser window when initially loading a post.<tiphead>Note</tiphead>When logged in, the account's \"fit images to window\" setting will override this option. This behavior can be changed with the \"override resize setting\" option under the preferences tab."),
418 post_resize_mode: newOption("dropdown", "width", "Resize Mode", "Choose how to shrink large post content to fit the browser window when initially loading a post.", {txtOptions:["Width:width", "Height:height", "Width & Height:all"]}),
419 post_tag_scrollbars: newOption("dropdown", 0, "Post Tag Scrollbars", "Limit the length of the sidebar tag lists for posts by restricting them to a set height in pixels. For lists that exceed the set height, a scrollbar will be added to allow the rest of the list to be viewed.<tiphead>Note</tiphead>When using \"remove tag headers\", this option will limit the overall length of the combined list.", {txtOptions:["Disabled:0"], numList:[50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500]}),
420 post_tag_titles: newOption("checkbox", false, "Post Tag Titles", "Change the page titles for posts to a full list of the post tags."),
421 quick_search: newOption("dropdown", "disabled", "Quick Search", "Add a new search box to the upper right corner of the window viewport that allows searching through the current thumbnails for specific posts. <tipdesc>Fade:</tipdesc> Fade all posts that don't match in the thumbnail listing. <tipdesc>Remove:</tipdesc> Remove all posts that don't match from the thumbnail listing. <tiphead>Directions</tiphead>Please read the \"thumbnail matching rules\" section under the help tab for information about creating searches. <br><br>The search starts minimized in the upper right corner. Left clicking the main icon will open and close the search. Right clicking the main icon will completely reset the search. Holding down shift while left clicking the main icon will toggle an active search's pinned status. Holding down control while left clicking the main icon will toggle an active search's negated status.<br><br>While open, the search can be entered/updated in the search box and the pinned status can be toggled by clicking the pushpin icon. Clicking the negative icon next to the pushpin icon will tell quick search to negate/invert the search being submitted. If no changes are made to an active search, submitting it a second time will reset the quick search. <tiphead>Notes</tiphead>Options labeled with \"pinned\" will make searches default to being pinned. <br><br>A pinned search will persist across other pages in the same browsing session for that tab until it ends or the search is unpinned. <br><br>When not set to disabled, the quick search can be opened by using the \"F\" hotkey. Additionally, an active search can be reset by using \"Shift + F\". Pressing \"Escape\" while the quick search is open will close it.", {txtOptions:["Disabled:disabled", "Fade:fade", "Fade (Pinned):fade pinned", "Remove:remove", "Remove (Pinned):remove pinned"]}),
422 remove_tag_headers: newOption("checkbox", false, "Remove Tag Headers", "Remove the \"copyrights\", \"characters\", and \"artist\" headers from the sidebar tag list."),
423 resize_link_style: newOption("dropdown", "full", "Resize Link Style", "Set how the resize links in the post sidebar options section will display. <tipdesc>Full:</tipdesc> Show the \"resize to window\", \"resize to window width\", and \"resize to window height\" links on separate lines. <tipdesc>Minimal:</tipdesc> Show the \"resize to window\" (W&H), \"resize to window width\" (W), and \"resize to window height\" (H) links on one line.", {txtOptions:["Full:full", "Minimal:minimal"]}),
424 search_add: newOption("dropdown", "disabled", "Search Add", "Modify the sidebar tag list by adding, removing, or replacing links in the sidebar tag list that modify the current search's tags. <tipdesc>Remove:</tipdesc> Remove any preexisting \"+\" and \"–\" links. <tipdesc>Link:</tipdesc> Add \"+\" and \"–\" links to modified versions of the current search that include or exclude their respective tags. <tipdesc>Toggle:</tipdesc> Add toggle links that modify the search box with their respective tags. Clicking a toggle link will switch between a tag being included (+), excluded (–), potentially included among other tags (~), and removed (»). Right clicking a toggle link will immediately remove its tag. If a tag already exists in the search box or gets entered/removed through alternative means, the toggle link will automatically update to reflect the tag's current status. <tiphead>Note</tiphead>The remove option is intended for users above the basic user level that want to remove the links. For users that can't normally see the links and do not wish to see them, this setting should be set to disabled.", {txtOptions:["Disabled:disabled", "Remove:remove", "Link:link", "Toggle:toggle"]}),
425 search_tag_scrollbars: newOption("dropdown", 0, "Search Tag Scrollbars", "Limit the length of the sidebar tag list for the search listing by restricting it to a set height in pixels. When the list exceeds the set height, a scrollbar will be added to allow the rest of the list to be viewed.", {txtOptions:["Disabled:0"], numList:[50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500]}),
426 show_banned: newOption("checkbox", false, "Show Banned Placeholders", "Display all banned posts in the search, pool, popular, favorites, comments, and favorite group listings by using \"hidden\" placeholders.<tiphead>Note</tiphead>This option only affects users below the gold account level."),
427 show_deleted: newOption("checkbox", false, "Show Deleted", "Display all deleted posts in the search, pool, popular, favorites, and favorite group listings. <tiphead>Note</tiphead>When using this option, your Danbooru account settings should have \"deleted post filter\" set to no and \"show deleted children\" set to yes in order to function properly and minimize connections to Danbooru."),
428 show_loli: newOption("checkbox", false, "Show Loli Placeholders", "Display loli posts in the search, pool, popular, favorites, comments, and favorite group listings by using \"hidden\" placeholders.<tiphead>Note</tiphead>This option only affects users below the gold account level."),
429 show_resized_notice: newOption("dropdown", "all", "Show Resized Notice", "Set which image type(s) the purple notice bar about image resizing is allowed to display on. <tiphead>Tip</tiphead>When a sample and original image are available for a post, a new option for swapping between the sample and original image becomes available in the sidebar options menu. Even if you disable the resized notice bar, you will always have access to its main function.", {txtOptions:["None (Disabled):none", "Original:original", "Sample:sample", "Original & Sample:all"]}),
430 show_shota: newOption("checkbox", false, "Show Shota Placeholders", "Display shota posts in the search, pool, popular, favorites, comments, and favorite group listings by using \"hidden\" placeholders.<tiphead>Note</tiphead>This option only affects users below the gold account level."),
431 show_toddlercon: newOption("checkbox", false, "Show Toddlercon Placeholders", "Display toddlercon posts in the search, pool, popular, favorites, comments, and favorite group listings by using \"hidden\" placeholders.<tiphead>Note</tiphead>This option only affects users below the gold account level."),
432 single_color_borders: newOption("checkbox", false, "Single Color Borders", "Only use one color for each thumbnail border."),
433 thumb_info: newOption("dropdown", "disabled", "Thumbnail Info", "Display the score (★), favorite count (♥), and rating (S, Q, or E) for a post with its thumbnail. <tipdesc>Below:</tipdesc> Display the extra information below thumbnails. <tipdesc>Hover:</tipdesc> Display the extra information upon hovering over a thumbnail's area. <tiphead>Note</tiphead>Extra information will not be added to the thumbnails in the comments listing since the score and rating are already visible there. Instead, the number of favorites will be added next to the existing score display.", {txtOptions:["Disabled:disabled", "Below:below", "Hover:hover"]}),
434 thumbnail_count: newOption("dropdown", 0, "Thumbnail Count", "Change the number of thumbnails that display in the search and favorites listings.", {txtOptions:["Disabled:0"], numRange:[1,200]}),
435 thumbnail_count_default: newOption("dropdown", 20, "Thumbnail Count Default", "Change the number of thumbnails that BBB should expect Danbooru to return per a page.<tiphead>Note</tiphead>This option only affects users above the basic account level. It should only be changed from 20 by users <b>that have changed their account setting for posts per page</b>.", {txtOptions:["Disabled:0"], numRange:[1,100]}),
436 track_new: newOption("checkbox", false, "Track New Posts", "Add a menu option titled \"new\" to the posts section submenu (between \"listing\" and \"upload\") that links to a customized search focused on keeping track of new posts.<tiphead>Note</tiphead>While browsing the new posts, the current page of posts is also tracked. If the new post listing is left, clicking the \"new\" link later on will attempt to pull up the posts where browsing was left off at.<tiphead>Tip</tiphead>If you would like to bookmark the new post listing, drag and drop the link to your bookmarks or right click it and bookmark/copy the location from the context menu."),
437 video_autoplay: newOption("dropdown", "on", "Video Autoplay", "Automatically play video posts. <tipdesc>Off:</tipdesc> All videos have to be started manually. <tipdesc>No Sound:</tipdesc> Videos without sound will start automatically and videos with sound have to be started manually. <tipdesc>On:</tipdesc> All videos will start automatically.", {txtOptions:["Off:off", "No Sound:nosound", "On:on"]}),
438 video_controls: newOption("checkbox", true, "Video Controls", "Show the controls for video posts."),
439 video_loop: newOption("dropdown", "nosound", "Video Looping", "Repeatedly play video posts. <tipdesc>Off:</tipdesc> All videos will only play once. <tipdesc>No Sound:</tipdesc> Videos without sound will repeat and videos with sound will only play once. <tipdesc>On:</tipdesc> All videos will repeat.", {txtOptions:["Off:off", "No Sound:nosound", "On:on"]}),
440 video_volume: newOption("dropdown", "disabled", "Video Volume", "Set the default volume of videos or remember your volume settings across videos. <tipdesc>Remember:</tipdesc> Set the video to the last volume level and muted status used. <tipdesc>Muted:</tipdesc> Always set the video volume to muted. <tipdesc>5% - 100%:</tipdesc> Always set the video volume level to the specified percent. <tiphead>Note</tiphead>This option can not control the volume of flash videos.", {txtOptions:["Disabled:disabled", "Remember:remember", "Muted:muted", "5%:0.05", "10%:0.1", "15%:0.15", "20%:0.2", "25%:0.25", "30%:0.30", "35%:0.35", "40%:0.4", "45%:0.45", "50%:0.5", "55%:0.55", "60%:0.6", "65%:0.65", "70%:0.7", "75%:0.75", "80%:0.8", "85%:0.85", "90%:0.9", "95%:0.95", "100%:1"]}),
441 collapse_sidebar_data: {post: {}, thumb: {}},
442 script_blacklisted_tags: "",
443 status_borders: borderSet(["deleted", true, "#000000", "solid", "post-status-deleted"], ["flagged", true, "#FF0000", "solid", "post-status-flagged"], ["pending", true, "#0000FF", "solid", "post-status-pending"], ["child", true, "#CCCC00", "solid", "post-status-has-parent"], ["parent", true, "#00FF00", "solid", "post-status-has-children"]),
444 tag_borders: borderSet(["loli", true, "#FFC0CB", "solid"], ["shota", true, "#66CCFF", "solid"], ["toddlercon", true, "#9370DB", "solid"], ["status:banned", true, "#000000", "solid"]),
445 tag_groups: groupSet(["hidden", "~loli ~shota ~toddlercon ~status:banned"]),
446 track_new_data: {viewed: 0, viewing: 1},
447 video_volume_data: {level: 1, muted: false}
448 },
449 quick_search: {
450 negated: false,
451 tags: ""
452 },
453 search_add: {
454 active_links: {},
455 links: {},
456 old: ""
457 },
458 sections: { // Setting sections and ordering.
459 blacklist_options: newSection("general", ["blacklist_session_toggle", "blacklist_post_display", "blacklist_thumb_mark", "blacklist_highlight_color", "blacklist_thumb_controls", "blacklist_smart_view", "blacklist_add_bars", "blacklist_video_playback", "blacklist_ignore_fav"], "Options"),
460 border_options: newSection("general", ["custom_tag_borders", "custom_status_borders", "single_color_borders", "border_width", "border_spacing"], "Options"),
461 browse: newSection("general", ["show_loli", "show_shota", "show_toddlercon", "show_banned", "show_deleted", "thumbnail_count", "thumb_info", "post_link_new_window"], "Post Browsing"),
462 control: newSection("general", ["load_sample_first", "alternate_image_swap", "image_swap_mode", "post_resize", "post_resize_mode", "post_drag_scroll", "autoscroll_post", "disable_embedded_notes", "video_volume", "video_autoplay", "video_loop", "video_controls"], "Post Control"),
463 endless: newSection("general", ["endless_default", "endless_session_toggle", "endless_separator", "endless_scroll_limit", "endless_remove_dup", "endless_pause_interval", "endless_fill", "endless_preload"], "Endless Pages"),
464 groups: newSection("group", "tag_groups", "Groups", "Tags that are frequently used together or that need to be coordinated between multiple places may be grouped together and saved here for use with the \"group\" metatag."),
465 notices: newSection("general", ["show_resized_notice", "minimize_status_notices", "hide_sign_up_notice", "hide_upgrade_notice", "hide_hidden_notice", "hide_tos_notice", "hide_comment_notice", "hide_tag_notice", "hide_upload_notice", "hide_pool_notice", "hide_ban_notice"], "Notices"),
466 sidebar: newSection("general", ["remove_tag_headers", "post_tag_scrollbars", "search_tag_scrollbars", "autohide_sidebar", "fixed_sidebar", "collapse_sidebar"], "Tag Sidebar"),
467 misc: newSection("general", ["direct_downloads", "track_new", "clean_links", "post_tag_titles", "search_add", "page_counter", "comment_score", "quick_search"], "Misc."),
468 misc_layout: newSection("general", ["fixed_paginator", "hide_fav_button", "add_popular_link", "add_random_post_link"], "Misc."),
469 script_settings: newSection("general", ["bypass_api", "manage_cookies", "enable_status_message", "enable_menu_autocomplete", "resize_link_style", "override_blacklist", "override_resize", "override_sample", "disable_tagged_filenames", "thumbnail_count_default"], "Script Settings"),
470 status_borders: newSection("border", "status_borders", "Custom Status Borders", "When using custom status borders, the borders can be edited here. For easy color selection, use one of the many free tools on the internet like <a target=\"_blank\" href=\"http://www.quackit.com/css/css_color_codes.cfm\">this one</a>."),
471 tag_borders: newSection("border", "tag_borders", "Custom Tag Borders", "When using custom tag borders, the borders can be edited here. For easy color selection, use one of the many free tools on the internet like <a target=\"_blank\" href=\"http://www.quackit.com/css/css_color_codes.cfm\">this one</a>.")
472 },
473 session: new Date().getTime(), // Provide a session ID in order to detect XML requests carrying over from other pages.
474 settings: {
475 changed: {}
476 },
477 timers: {},
478 user: {} // User settings.
479 };
480
481 localStorageCheck();
482
483 loadSettings(); // Load user settings.
484
485 // Location variables.
486 var gLoc = danbLoc(); // Current location
487 var gLocRegex = new RegExp("\\b" + gLoc + "\\b");
488
489 // Script variables.
490 // Global
491 var show_loli = (isGoldLevel() ? true : bbb.user.show_loli);
492 var show_shota = (isGoldLevel() ? true : bbb.user.show_shota);
493 var show_toddlercon = (isGoldLevel() ? true : bbb.user.show_toddlercon);
494 var show_banned = (isGoldLevel() ? true : bbb.user.show_banned);
495 var deleted_shown = (gLoc === "search" && /^(?:any|deleted)$/i.test(getTagVar("status"))); // Check whether deleted posts are shown by default.
496 var show_deleted = deleted_shown || bbb.user.show_deleted;
497 var direct_downloads = bbb.user.direct_downloads;
498 var post_link_new_window = bbb.user.post_link_new_window;
499
500 var blacklist_session_toggle = bbb.user.blacklist_session_toggle;
501 var blacklist_post_display = bbb.user.blacklist_post_display;
502 var blacklist_thumb_mark = bbb.user.blacklist_thumb_mark;
503 var blacklist_highlight_color = bbb.user.blacklist_highlight_color;
504 var blacklist_ignore_fav = bbb.user.blacklist_ignore_fav;
505 var blacklist_add_bars = bbb.user.blacklist_add_bars;
506 var blacklist_thumb_controls = bbb.user.blacklist_thumb_controls;
507 var blacklist_smart_view = bbb.user.blacklist_smart_view;
508 var blacklist_video_playback = bbb.user.blacklist_video_playback;
509
510 var custom_tag_borders = bbb.user.custom_tag_borders;
511 var custom_status_borders = bbb.user.custom_status_borders;
512 var single_color_borders = bbb.user.single_color_borders;
513 var border_spacing = bbb.user.border_spacing;
514 var border_width = bbb.user.border_width;
515 var clean_links = bbb.user.clean_links;
516 var comment_score = bbb.user.comment_score;
517 var thumb_info = bbb.user.thumb_info;
518 var autohide_sidebar = gLocRegex.test(bbb.user.autohide_sidebar);
519 var fixed_sidebar = gLocRegex.test(bbb.user.fixed_sidebar);
520 var fixed_paginator = bbb.user.fixed_paginator;
521 var collapse_sidebar = bbb.user.collapse_sidebar;
522 var page_counter = bbb.user.page_counter;
523 var quick_search = bbb.user.quick_search;
524
525 var bypass_api = bbb.user.bypass_api;
526 var manage_cookies = bbb.user.manage_cookies;
527 var enable_menu_autocomplete = bbb.user.enable_menu_autocomplete;
528 var enable_status_message = bbb.user.enable_status_message;
529 var resize_link_style = bbb.user.resize_link_style;
530 var override_blacklist = bbb.user.override_blacklist;
531 var override_resize = bbb.user.override_resize;
532 var override_sample = bbb.user.override_sample;
533 var disable_tagged_filenames = bbb.user.disable_tagged_filenames;
534 var track_new = bbb.user.track_new;
535
536 var add_popular_link = bbb.user.add_popular_link;
537 var add_random_post_link = bbb.user.add_random_post_link;
538 var hide_fav_button = bbb.user.hide_fav_button;
539 var show_resized_notice = bbb.user.show_resized_notice;
540 var hide_sign_up_notice = bbb.user.hide_sign_up_notice;
541 var hide_upgrade_notice = bbb.user.hide_upgrade_notice;
542 var minimize_status_notices = bbb.user.minimize_status_notices;
543 var hide_tos_notice = bbb.user.hide_tos_notice;
544 var hide_comment_notice = bbb.user.hide_comment_notice;
545 var hide_tag_notice = bbb.user.hide_tag_notice;
546 var hide_upload_notice = bbb.user.hide_upload_notice;
547 var hide_pool_notice = bbb.user.hide_pool_notice;
548 var hide_ban_notice = bbb.user.hide_ban_notice;
549 var hide_hidden_notice = bbb.user.hide_hidden_notice;
550
551 // Search
552 var search_add = bbb.user.search_add;
553 var search_tag_scrollbars = bbb.user.search_tag_scrollbars;
554 var thumbnail_count = bbb.user.thumbnail_count;
555 var thumbnail_count_default = (isGoldLevel() ? bbb.user.thumbnail_count_default : 20);
556
557 // Post
558 var alternate_image_swap = bbb.user.alternate_image_swap;
559 var post_resize = accountSettingCheck("post_resize");
560 var post_resize_mode = bbb.user.post_resize_mode;
561 var post_drag_scroll = bbb.user.post_drag_scroll;
562 var load_sample_first = accountSettingCheck("load_sample_first");
563 var remove_tag_headers = bbb.user.remove_tag_headers;
564 var post_tag_scrollbars = bbb.user.post_tag_scrollbars;
565 var post_tag_titles = bbb.user.post_tag_titles;
566 var autoscroll_post = bbb.user.autoscroll_post;
567 var image_swap_mode = bbb.user.image_swap_mode;
568 var disable_embedded_notes = bbb.user.disable_embedded_notes;
569 var video_autoplay = bbb.user.video_autoplay;
570 var video_controls = bbb.user.video_controls;
571 var video_loop = bbb.user.video_loop;
572 var video_volume = bbb.user.video_volume;
573
574 // Endless
575 var endless_default = bbb.user.endless_default;
576 var endless_fill = bbb.user.endless_fill;
577 var endless_pause_interval = bbb.user.endless_pause_interval;
578 var endless_preload = bbb.user.endless_preload;
579 var endless_remove_dup = bbb.user.endless_remove_dup;
580 var endless_scroll_limit = bbb.user.endless_scroll_limit;
581 var endless_separator = bbb.user.endless_separator;
582 var endless_session_toggle = bbb.user.endless_session_toggle;
583
584 // Stored data
585 var status_borders = bbb.user.status_borders;
586 var tag_borders = bbb.user.tag_borders;
587 var collapse_sidebar_data = bbb.user.collapse_sidebar_data;
588 var tag_groups = bbb.user.tag_groups;
589 var track_new_data = bbb.user.track_new_data;
590 var video_volume_data = bbb.user.video_volume_data;
591 var script_blacklisted_tags = accountSettingCheck("script_blacklisted_tags");
592
593 // Other data
594 var bbbHiddenImg = "data:image/png;base64,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";
595 var bbbBlacklistImg = "data:image/png;base64,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";
596 var bbbBlacklistIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAAkFBMVEUAAAD////////////////////////////////////////////////////////////////p6en////////////////////////////////////////////////////////+/v7///////////////////////////////////////////////////////////97JICZAAAAL3RSTlMACAQBoUSi5QcnMfmnsMW9VQno+vjsxrwstPyzMhOpSxS65vX06czqQf7NvqbzQKZY7GsAAADASURBVHhefZDnDoJAEITn8Kh3FAEFQbH3Mu//doaNLSFxfn7J7hT80cgJlAqc0S8bu55P+p47/rJQ8yUdflhCHq/WpjmZvKjSPPOyBFbZlNRKPFySzTaOANQJ6fZujsf9YUcjNMvpOQACn+kyLmjaObBI6QcAFGkRxYblLAIsqd4Q0ayUDzeBcr4A5q2h6U5Vfy5GeQbIh8l6I0YSKal72k3uhUSS8OQ0WwGPddFQq2/NPLW22rxrDgcZTvd35KGevk8VfmeGhUQAAAAASUVORK5CYII=";
597
598 /* "INIT" */
599 modifyDanbScript();
600
601 customCSS(); // Contains the portions related to notices.
602
603 thumbInfo();
604
605 addPopularLink();
606
607 removeTagHeaders();
608
609 searchAdd();
610
611 minimizeStatusNotices();
612
613 postTagTitles();
614
615 trackNew();
616
617 injectSettings();
618
619 accountUpdateWatch();
620
621 modifyPage();
622
623 autohideSidebar();
624
625 pageCounter();
626
627 quickSearch();
628
629 postLinkNewWindow();
630
631 commentScoreInit();
632
633 postLinkQuery();
634
635 postDDL();
636
637 fixLimit();
638
639 bbbHotkeys();
640
641 endlessInit();
642
643 delayMe(formatThumbnails); // Delayed to allow Danbooru to run first.
644
645 delayMe(blacklistInit); // Delayed to allow Danbooru to run first.
646
647 delayMe(fixedSidebar); // Delayed to allow Danbooru layout to finalize.
648
649 delayMe(collapseSidebar); // Delayed to allow Danbooru layout to finalize.
650
651 delayMe(fixedPaginator); // Delayed to allow Danbooru layout to finalize.
652
653 /* Functions */
654
655 /* Functions for XML API info */
656 function searchJSON(mode, optArg) {
657 // Figure out the desired URL for a JSON API request, trigger any necessary xml flag, and update the status message.
658 var url = location.href.split("#", 1)[0];
659 var idCache, idList, idSearch, page; // If/else variables.
660
661 if (mode === "search" || mode === "favorites") {
662 url = (allowUserLimit() ? updateURLQuery(url, {limit: thumbnail_count}) : url);
663 bbb.flags.thumbs_xml = true;
664
665 if (mode === "search")
666 fetchJSON(url.replace(/\/?(?:posts)?\/?(?:\?|$)/, "/posts.json?"), "search");
667 else if (mode === "favorites")
668 fetchJSON(url.replace(/\/favorites\/?(?:\?|$)/, "/favorites.json?"), "favorites");
669
670 bbbStatus("posts", "new");
671 }
672 else if (mode === "popular" || mode === "popular_view") {
673 bbb.flags.thumbs_xml = true;
674
675 fetchJSON(url.replace(/\/(popular_view|popular)\/?/, "/$1.json"), mode);
676 bbbStatus("posts", "new");
677 }
678 else if (mode === "pool" || mode === "favorite_group") {
679 idCache = getIdCache();
680 bbb.flags.thumbs_xml = true;
681
682 if (idCache)
683 searchJSON(mode + "_search", {post_ids: idCache});
684 else // Get a new cache.
685 fetchJSON(url.replace(/\/(pools|favorite_groups)\/(\d+)/, "/$1/$2.json"), mode + "_cache", mode + "_search");
686
687 bbbStatus("posts", "new");
688 }
689 else if (mode === "pool_search" || mode === "favorite_group_search") {
690 page = Number(getVar("page")) || 1;
691 idList = optArg.post_ids.split(" ");
692 idSearch = idList.slice((page - 1) * thumbnail_count_default, page * thumbnail_count_default);
693
694 fetchJSON("/posts.json?tags=status:any+id:" + idSearch.join(","), mode, idSearch);
695 }
696 else if (mode === "endless") {
697 bbb.flags.endless_xml = true;
698
699 if (gLoc === "pool" || gLoc === "favorite_group") {
700 idCache = getIdCache();
701
702 if (idCache)
703 searchJSON("endless_" + gLoc + "_search", {post_ids: idCache});
704 else // Get a new cache.
705 fetchJSON(url.replace(/\/(pools|favorite_groups)\/(\d+)/, "/$1/$2.json"), gLoc + "_cache", "endless_" + gLoc + "_search");
706 }
707 else {
708 url = endlessNexURL();
709
710 fetchJSON(url.replace(/(\?)|$/, ".json$1"), "endless");
711 }
712
713 bbbStatus("posts", "new");
714 }
715 else if (mode === "endless_pool_search" || mode === "endless_favorite_group_search") {
716 idList = optArg.post_ids.split(" ");
717 page = Number(getVar("page", endlessNexURL())); // If a pool gets over 1000 pages, I have no idea what happens for regular users. Biggest pool is currently around 400 pages so we won't worry about that for the time being.
718 idSearch = idList.slice((page - 1) * thumbnail_count_default, page * thumbnail_count_default);
719
720 fetchJSON("/posts.json?tags=status:any+id:" + idSearch.join(","), "endless", idSearch);
721 }
722 else if (mode === "comments") {
723 fetchJSON(url.replace(/\/comments\/?/, "/comments.json"), "comments");
724 bbbStatus("posts", "new");
725 }
726 else if (mode === "parent" || mode === "child") {
727 var parentUrl = "/posts.json?limit=200&tags=status:any+parent:" + optArg;
728
729 fetchJSON(parentUrl, mode, optArg);
730 bbbStatus("posts", "new");
731 }
732 }
733
734 function fetchJSON(url, mode, optArg, session, retries) {
735 // Retrieve JSON.
736 var xmlhttp = new XMLHttpRequest();
737 var xmlRetries = retries || 0;
738 var xmlSession = session || bbb.session;
739
740 if (xmlhttp !== null) {
741 xmlhttp.onreadystatechange = function() {
742 if (xmlSession !== bbb.session) // If we end up receiving an xml response from a different page, reject it.
743 xmlhttp.abort();
744 else if (xmlhttp.readyState === 4) { // 4 = "loaded"
745 if (xmlhttp.status === 200) { // 200 = "OK"
746 var xml = parseJson(xmlhttp.responseText, {});
747 //MOD
748 for (var i = 0, len = xml.length; i < len; i++) {
749 if (typeof xml[i]['md5'] === 'undefined'){
750 var md5_ext = window.localStorage.getItem(xml[i]['id']);
751 if (md5_ext == null)
752 continue;
753 md5_ext = md5_ext.split('.');
754 let md5 = md5_ext[0];
755 xml[i]['md5'] = md5;
756 xml[i]['preview_file_url'] = "https://raikou4.donmai.us/preview/" + md5[0] + md5[1] + "/" + md5[2] + md5[3] + "/" + md5 + ".jpg";
757 }
758}
759
760 // Update status message.
761 if (mode === "search" || mode === "popular" || mode === "popular_view" || mode === "favorites" || mode === "pool_search" || mode === "favorite_group_search") {
762 bbb.flags.thumbs_xml = false;
763
764 parseListing(formatInfoArray(xml), optArg);
765 }
766 else if (mode === "pool_cache" || mode === "favorite_group_cache") {
767 var collId = location.href.match(/\/(?:pools|favorite_groups)\/(\d+)/)[1];
768
769 sessionStorage.bbbSetItem("bbb_" + mode + "_" + collId, new Date().getTime() + " " + xml.post_ids);
770 searchJSON(optArg, xml);
771 }
772 else if (mode === "endless") {
773 bbb.flags.endless_xml = false;
774
775 endlessXMLJSONHandler(formatInfoArray(xml), optArg);
776 }
777 else if (mode === "comments")
778 parseComments(formatInfoArray(xml));
779 else if (mode === "parent" || mode === "child")
780 parseRelations(formatInfoArray(xml), mode, optArg);
781
782 if (mode !== "pool_cache" && mode !== "favorite_group_cache")
783 bbbStatus("posts", "done");
784 }
785 else {
786 if (xmlhttp.status === 403 || xmlhttp.status === 401) {
787 bbbNotice('Error retrieving post information. Access denied. You must be logged in to a Danbooru account to access the API for hidden image information and direct downloads. <br><span style="font-size: smaller;">(<span><a href="#" id="bbb-bypass-api-link">Do not warn me again and automatically bypass API features in the future.</a></span>)</span>', -1);
788 document.getElementById("bbb-bypass-api-link").addEventListener("click", function(event) {
789 if (event.button !== 0)
790 return;
791
792 updateSettings("bypass_api", true);
793 this.parentNode.innerHTML = "Settings updated. You may change this setting under the preferences tab in the settings panel.";
794 event.preventDefault();
795 }, false);
796 bbbStatus("posts", "error");
797 }
798 else if (xmlhttp.status === 421) {
799 bbbNotice("Error retrieving post information. Your Danbooru API access is currently throttled. Please try again later.", -1);
800 bbbStatus("posts", "error");
801 }
802 else if (!bbb.flags.unloading) {
803 if (xmlhttp.status !== 0 && xmlRetries < 1) {
804 xmlRetries++;
805 fetchJSON(url, mode, optArg, xmlSession, xmlRetries);
806 }
807 else {
808 var linkId = uniqueIdNum(); // Create a unique ID.
809 var noticeMsg = bbbNotice('Error retrieving post information (JSON Code: ' + xmlhttp.status + ' ' + xmlhttp.statusText + '). ' + (xmlhttp.status === 0 ? 'The connection was unexpectedly cancelled or timed out. ' : '') + '(<a id="' + linkId + '" href="#">Retry</a>)', -1);
810
811 bbbStatus("posts", "error");
812
813 document.getElementById(linkId).addEventListener("click", function(event) {
814 if (event.button !== 0)
815 return;
816
817 closeBbbNoticeMsg(noticeMsg);
818 searchJSON(mode, optArg);
819 event.preventDefault();
820 }, false);
821 }
822 }
823 }
824 }
825 };
826 xmlhttp.open("GET", url, true);
827 xmlhttp.timeout = 120000;
828 xmlhttp.send(null);
829 }
830 }
831
832 function parseListing(xml, optArg) {
833 // Use JSON results for thumbnail listings.
834 var posts = xml;
835 var orderedIds = (gLoc === "pool" || gLoc === "favorite_group" ? optArg : undefined);
836
837 if (!posts[0])
838 return;
839
840 // Thumb preparation.
841 var newThumbs = createThumbListing(posts, orderedIds);
842
843 // Update the existing thumbnails with new ones.
844 updateThumbListing(newThumbs);
845
846 // Fix the paginator. The paginator isn't always in the new container, so run this on the whole page after the new container is inserted.
847 fixPaginator();
848
849 // Update the URL with the limit value.
850 fixURLLimit();
851 }
852
853 function parsePost() {
854 // Take a post's info and alter its page.
855 var postInfo = bbb.post.info = document.bbbInfo();
856 //MOD
857 if (postInfo['md5'] == ""){
858 var md5_ext = window.localStorage.getItem(postInfo['id']);
859 postInfo['file_img_src'] = postInfo['file_url'] = (postInfo['id'] < 1000000 ? "/cached" : "") + "/data//" + md5_ext;
860 if (postInfo.file_ext === "zip"){
861 load_sample_first = true;
862 md5_ext = md5_ext.split('.');
863 postInfo['large_file_img_src'] = postInfo['file_url'] = "/data/sample/sample-" + md5_ext[0] + ".webm";
864 }
865}
866 var imgContainer = document.getElementById("image-container");
867
868 if (!imgContainer || !postInfo) {
869 bbbNotice("Post content could not be located.", -1);
870 return;
871 }
872
873 if (!postInfo.file_url || safebPostTest(postInfo)) { // Modify everything except the post content if we're on Safebooru and the image isn't safe or if the image is hidden.
874 // Enable the "Toggle Notes", "Random Post", and "Find similar" options for logged out users.
875 fixOptionsSection();
876
877 // Add the random post link.
878 addRandomPostLink();
879
880 // Replace the "resize to window" link with new resize links.
881 modifyResizeLink();
882
883 // Auto position the content if desired.
884 autoscrollPost();
885
886 // Blacklist.
887 blacklistUpdate();
888
889 // Fix the parent/child notice(s).
890 checkRelations();
891 }
892 else {
893 // Enable the "Toggle Notes", "Random Post", and "Find similar" options for logged out users.
894 fixOptionsSection();
895
896 // Fix the post links in the sidebar.
897 fixPostDownloadLinks();
898
899 // Add the random post link.
900 addRandomPostLink();
901
902 // Replace the "resize to window" link with new resize links.
903 modifyResizeLink();
904
905 // Keep any original video from continuing to play/download after being removed.
906 var origVideo = imgContainer.getElementsByTagName("video")[0];
907
908 if (origVideo) {
909 origVideo.pause();
910 origVideo.src = "about:blank";
911 origVideo.load();
912 }
913
914 // Create content.
915 if (postInfo.file_ext === "swf") // Create flash object.
916 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <object height="' + postInfo.image_height + '" width="' + postInfo.image_width + '"> <params name="movie" value="' + postInfo.file_img_src + '"> <embed allowscriptaccess="never" src="' + postInfo.file_img_src + '" height="' + postInfo.image_height + '" width="' + postInfo.image_width + '"> </params> </object> <p><a href="' + postInfo.file_img_src + '">Save this flash (right click and save)</a></p>';
917 else if (postInfo.file_ext === "webm" || postInfo.file_ext === "mp4") { // Create video
918 var playerControls = (video_controls ? ' controls="controls" ' : '');
919 var playerLoop = (video_loop === "on" || (!postInfo.has_sound && video_loop === "nosound") ? ' loop="loop" ' : '');
920 var playerAutoplay = (video_autoplay === "on" || (!postInfo.has_sound && video_autoplay === "nosound") ? ' autoplay="autoplay" ' : '');
921
922 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <video id="image"' + playerAutoplay + playerLoop + playerControls + 'src="' + postInfo.file_img_src + '" height="' + postInfo.image_height + '" width="' + postInfo.image_width + '"></video> <p><a href="' + postInfo.file_img_src + '">Save this video (right click and save)</a></p>';
923
924 videoVolume();
925 }
926 else if (postInfo.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(postInfo.tag_string)) { // Create ugoira
927 var useUgoiraOrig = getVar("original");
928
929 // Get rid of all the old events handlers.
930 if (Danbooru.Ugoira && Danbooru.Ugoira.player)
931 $(Danbooru.Ugoira.player).unbind();
932
933 if ((load_sample_first && useUgoiraOrig !== "1") || useUgoiraOrig === "0") { // Load sample webm version.
934 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <video id="image" autoplay="autoplay" loop="loop" controls="controls" src="' + postInfo.large_file_img_src + '" height="' + postInfo.image_height + '" width="' + postInfo.image_width + '" data-fav-count="' + postInfo.fav_count + '" data-flags="' + postInfo.flags + '" data-has-active-children="' + postInfo.has_active_children + '" data-has-children="' + postInfo.has_children + '" data-large-height="' + postInfo.large_height + '" data-large-width="' + postInfo.large_width + '" data-original-height="' + postInfo.image_height + '" data-original-width="' + postInfo.image_width + '" data-rating="' + postInfo.rating + '" data-score="' + postInfo.score + '" data-tags="' + postInfo.tag_string + '" data-pools="' + postInfo.pool_string + '" data-uploader="' + postInfo.uploader_name + '"></video> <p><a href="' + postInfo.large_file_img_src + '">Save this video (right click and save)</a> | <a href="' + updateURLQuery(location.href, {original: "1"}) + '">View original</a> | <a href="#" id="bbb-note-toggle">Toggle notes</a></p>';
935
936 // Prep the "toggle notes" link.
937 noteToggleLinkInit();
938 }
939 else { // Load original ugoira version.
940 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <canvas data-ugoira-content-type="' + postInfo.pixiv_ugoira_frame_data.content_type.replace(/"/g, """) + '" data-ugoira-frames="' + JSON.stringify(postInfo.pixiv_ugoira_frame_data.data).replace(/"/g, """) + '" data-fav-count="' + postInfo.fav_count + '" data-flags="' + postInfo.flags + '" data-has-active-children="' + postInfo.has_active_children + '" data-has-children="' + postInfo.has_children + '" data-large-height="' + postInfo.image_height + '" data-large-width="' + postInfo.image_width + '" data-original-height="' + postInfo.image_height + '" data-original-width="' + postInfo.image_width + '" data-rating="' + postInfo.rating + '" data-score="' + postInfo.score + '" data-tags="' + postInfo.tag_string + '" data-pools="' + postInfo.pool_string + '" data-uploader="' + postInfo.uploader_name + '" height="' + postInfo.image_height + '" width="' + postInfo.image_width + '" id="image"></canvas> <div id="ugoira-controls"> <div id="ugoira-control-panel" style="width: ' + postInfo.image_width + 'px; min-width: 350px;"> <button id="ugoira-play" name="button" style="display: none;" type="submit">Play</button> <button id="ugoira-pause" name="button" type="submit">Pause</button> <div id="seek-slider" style="width: ' + (postInfo.image_width - 81) + 'px; min-width: 269px;"></div> </div> <p id="save-video-link"><a href="' + postInfo.large_file_img_src + '">Save as video (right click and save)</a> | <a href="' + updateURLQuery(location.href, {original: "0"}) + '">View sample</a> | <a href="#" id="bbb-note-toggle">Toggle notes</a></p> </div>';
941
942 // Make notes toggle when clicking the ugoira animation.
943 noteToggleInit();
944
945 // Prep the "toggle notes" link. The "toggle notes" link is added here just for consistency's sake.
946 noteToggleLinkInit();
947
948 if (postInfo.pixiv_ugoira_frame_data.data && Danbooru.Ugoira && Danbooru.Ugoira.create_player) // Set up the post.
949 Danbooru.Ugoira.create_player(postInfo.pixiv_ugoira_frame_data.content_type, postInfo.pixiv_ugoira_frame_data.data, postInfo.file_url);
950 }
951 }
952 else if (!postInfo.image_height) // Create manual download.
953 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div><p><a href="' + postInfo.file_img_src + '">Save this file (right click and save)</a></p>';
954 else { // Create image
955 var newWidth, newHeight, newUrl; // If/else variables.
956
957 if (load_sample_first && postInfo.has_large) {
958 newWidth = postInfo.large_width;
959 newHeight = postInfo.large_height;
960 newUrl = postInfo.large_file_img_src;
961 }
962 else {
963 newWidth = postInfo.image_width;
964 newHeight = postInfo.image_height;
965 newUrl = postInfo.file_img_src;
966 }
967
968 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <img alt="' + postInfo.tag_string_desc + '" data-fav-count="' + postInfo.fav_count + '" data-flags="' + postInfo.flags + '" data-has-active-children="' + postInfo.has_active_children + '" data-has-children="' + postInfo.has_children + '" data-large-height="' + postInfo.large_height + '" data-large-width="' + postInfo.large_width + '" data-original-height="' + postInfo.image_height + '" data-original-width="' + postInfo.image_width + '" data-rating="' + postInfo.rating + '" data-score="' + postInfo.score + '" data-tags="' + postInfo.tag_string + '" data-pools="' + postInfo.pool_string + '" data-uploader="' + postInfo.uploader_name + '" height="' + newHeight + '" width="' + newWidth + '" id="image" src="' + newUrl + '" /> <img src="about:blank" height="1" width="1" id="bbb-loader" style="position: absolute; right: 0px; top: 0px; display: none;" />';
969
970 bbb.el.bbbLoader = document.getElementById("bbb-loader");
971
972 // Create/replace the elements related to image swapping and set them up.
973 swapImageInit();
974
975 if (alternate_image_swap) // Make sample/original images swap when clicking the image.
976 alternateImageSwap();
977 else // Make notes toggle when clicking the image.
978 noteToggleInit();
979 }
980
981 // Enable drag scrolling.
982 dragScrollInit();
983
984 // Resize the content if desired.
985 if (post_resize)
986 resizePost(post_resize_mode);
987
988 // Enable translation mode.
989 translationModeInit();
990
991 // Disable embedded notes.
992 disableEmbeddedNotes();
993
994 // Load/reload notes.
995 Danbooru.Note.load_all("bbb");
996
997 // Auto position the content if desired.
998 autoscrollPost();
999
1000 // Blacklist.
1001 blacklistUpdate();
1002
1003 // Fix the parent/child notice(s).
1004 checkRelations();
1005 }
1006 }
1007
1008 function parseComments(xml) {
1009 // Fix missing comments by inserting them into their appropriate position.
1010 var postsInfo = xml;
1011 var numPosts = postsInfo.length;
1012 var expectedPosts = numPosts;
1013 var existingPosts = getPosts();
1014 var eci = 0;
1015
1016 for (var i = 0; i < numPosts; i++) {
1017 var postInfo = postsInfo[i];
1018 var existingPost = existingPosts[eci];
1019
1020 if (!existingPost || String(postInfo.id) !== existingPost.bbbInfo("id")) {
1021 if (!/(?:^|\s)(?:loli|shota|toddlercon)(?:$|\s)/.test(postInfo.tag_string) && !postInfo.is_banned) // API post isn't hidden and doesn't exist on the page. Skip it and try to find where the page's info matches up.
1022 continue;
1023 else if ((!show_loli && /(?:^|\s)loli(?:$|\s)/.test(postInfo.tag_string)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(postInfo.tag_string)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(postInfo.tag_string)) || (!show_banned && postInfo.is_banned) || safebPostTest(postInfo)) { // Skip hidden posts if the user has selected to do so.
1024 expectedPosts--;
1025 continue;
1026 }
1027
1028 // Prepare the post information.
1029 var tagLinks = postInfo.tag_string.bbbSpacePad();
1030 var generalTags = postInfo.tag_string_general.split(" ");
1031 var artistTags = postInfo.tag_string_artist.split(" ");
1032 var copyrightTags = postInfo.tag_string_copyright.split(" ");
1033 var characterTags = postInfo.tag_string_character.split(" ");
1034 var metaTags = postInfo.tag_string_meta.split(" ");
1035 var limit = (thumbnail_count ? "&limit=" + thumbnail_count : "");
1036 var j, jl, tag; // Loop variables.
1037
1038 for (j = 0, jl = generalTags.length; j < jl; j++) {
1039 tag = generalTags[j];
1040 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-0"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
1041 }
1042
1043 for (j = 0, jl = artistTags.length; j < jl; j++) {
1044 tag = artistTags[j];
1045 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-1"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
1046 }
1047
1048 for (j = 0, jl = copyrightTags.length; j < jl; j++) {
1049 tag = copyrightTags[j];
1050 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-3"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
1051 }
1052
1053 for (j = 0, jl = characterTags.length; j < jl; j++) {
1054 tag = characterTags[j];
1055 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-4"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
1056 }
1057
1058 for (j = 0, jl = metaTags.length; j < jl; j++) {
1059 tag = metaTags[j];
1060 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-5"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
1061 }
1062
1063 // Create the new post.
1064 var childSpan = document.createElement("span");
1065
1066 childSpan.innerHTML = '<div id="post_' + postInfo.id + '" class="post post-preview' + postInfo.thumb_class + '" data-tags="' + postInfo.tag_string + '" data-pools="' + postInfo.pool_string + '" data-uploader="' + postInfo.uploader_name + '" data-rating="' + postInfo.rating + '" data-flags="' + postInfo.flags + '" data-score="' + postInfo.score + '" data-parent-id="' + postInfo.parent_id + '" data-has-children="' + postInfo.has_children + '" data-id="' + postInfo.id + '" data-has-sound="' + postInfo.has_sound + '" data-width="' + postInfo.image_width + '" data-height="' + postInfo.image_height + '" data-approver-id="' + postInfo.approver_id + '" data-fav-count="' + postInfo.fav_count + '" data-pixiv-id="' + postInfo.pixiv_id + '" data-md5="' + postInfo.md5 + '" data-file-ext="' + postInfo.file_ext + '" data-file-url="' + postInfo.file_url + '" data-large-file-url="' + postInfo.large_file_url + '" data-preview-file-url="' + postInfo.preview_file_url + '"> <div class="preview"> <a href="/posts/' + postInfo.id + '"> <img src="' + postInfo.preview_img_src + '" /> </a> </div> <div class="comments-for-post" data-post-id="' + postInfo.id + '"> <div class="header"> <div class="row"> <span class="info"> <strong>Date</strong> <time datetime="' + postInfo.created_at + '" title="' + postInfo.created_at.replace(/(.+)T(.+)\..+-(.+)/, "$1 $2 -$3") + '">' + postInfo.created_at.replace(/(.+)T(.+):.+-.+/, "$1 $2") + '</time> </span> <span class="info"> <strong>Rating</strong> ' + postInfo.rating + ' </span> <span class="info"> <strong>Score</strong> <span> <span id="score-for-post-' + postInfo.id + '">' + postInfo.score + '</span> </span> </span> </div> <div class="row list-of-tags"> <strong>Tags</strong>' + tagLinks + '</div> </div> </div> <div class="clearfix"></div> </div>';
1067
1068 // Prepare thumbnails.
1069 prepThumbnails(childSpan);
1070
1071 if (!existingPost) // There isn't a next post so append the new post to the end before the paginator.
1072 document.getElementById("a-index").insertBefore(childSpan.firstElementChild, getPaginator());
1073 else // Insert new post before the post that should follow it.
1074 existingPost.parentNode.insertBefore(childSpan.firstElementChild, existingPost);
1075
1076 // Get the comments and image info.
1077 searchPages("post_comments", postInfo.id);
1078 }
1079
1080 eci++;
1081 }
1082
1083 // If we don't have the expected number of posts, the API info and page are too out of sync. (Message disabled to work around deleted comments until an accurate method is worked out.)
1084 // if (existingPosts.length !== expectedPosts)
1085 // bbbNotice("Loading of hidden post(s) failed. Please refresh.", -1);
1086 }
1087
1088 function parseRelations(xml, mode, parentId) {
1089 // Create a new parent/child notice.
1090 var posts = xml;
1091 var activePost = bbb.post.info;
1092 var numPosts = posts.length;
1093 var relationCookie = getCookie()["show-relationship-previews"];
1094 var showPreview = (relationCookie === undefined || relationCookie === "1");
1095 var childSpan = document.createElement("span");
1096 var query = "?tags=parent:" + parentId + (show_deleted ? "+status:any" : "") + (thumbnail_count ? "&limit=" + thumbnail_count : "");
1097 var thumbs = "";
1098 var forceShowDeleted = activePost.is_deleted; // If the parent is deleted or the active post is deleted, all deleted posts are shown.
1099 var parentDeleted = false;
1100 var isSafebooru = (location.host.indexOf("safebooru") > -1);
1101 var target, previewLinkId, previewLinkTxt, previewId, classes, msg, displayStyle; // If/else variables.
1102 var i, post; // Loop variables.
1103
1104 // Figure out if the parent is deleted.
1105 for (i = 0; i < numPosts; i++) {
1106 post = posts[i];
1107
1108 if (post.id === parentId) {
1109 parentDeleted = post.is_deleted;
1110 forceShowDeleted = forceShowDeleted || parentDeleted;
1111 }
1112 }
1113
1114 // Set up the notice variables.
1115 if (showPreview) {
1116 previewLinkTxt = "« hide";
1117 displayStyle = "block";
1118 }
1119 else {
1120 previewLinkTxt = "show »";
1121 displayStyle = "none";
1122 }
1123
1124 if (mode === "child") {
1125 target = document.getElementsByClassName("notice-child")[0];
1126 previewLinkId = "has-parent-relationship-preview-link";
1127 previewId = "has-parent-relationship-preview";
1128 classes = "notice-child";
1129
1130 if (!isSafebooru) {
1131 if (numPosts)
1132 msg = 'This post belongs to a <a href="/posts' + query + '">parent</a>' + (parentDeleted ? " (deleted)" : "");
1133
1134 if (numPosts === 3)
1135 msg += ' and has <a href="/posts' + query + '">a sibling</a>';
1136 else if (numPosts > 3)
1137 msg += ' and has <a href="/posts' + query + '">' + (numPosts - 2) + ' siblings</a>';
1138 }
1139 else {
1140 var parentNotSafe = true;
1141
1142 for (i = 0; i < numPosts; i++) {
1143 if (posts[i].id === parentId)
1144 parentNotSafe = false;
1145 }
1146
1147 var siblingLimit = (parentNotSafe ? 2 : 3);
1148
1149 if (numPosts)
1150 msg = 'This post belongs to a <a href="/posts' + query + '">parent</a>' + (parentNotSafe ? " (not safe)" : (parentDeleted ? " (deleted)" : ""));
1151
1152 if (numPosts === siblingLimit)
1153 msg += ' and has <a href="/posts' + query + '">a sibling</a>';
1154 else if (numPosts > siblingLimit)
1155 msg += ' and has <a href="/posts' + query + '">' + (numPosts - 2) + ' siblings</a>';
1156 }
1157 }
1158 else if (mode === "parent") {
1159 target = document.getElementsByClassName("notice-parent")[0];
1160 previewLinkId = "has-children-relationship-preview-link";
1161 previewId = "has-children-relationship-preview";
1162 classes = "notice-parent";
1163
1164 if (!isSafebooru) {
1165 if (numPosts === 2)
1166 msg = 'This post has <a href="/posts' + query + '">a child</a>';
1167 else if (numPosts > 2)
1168 msg = 'This post has <a href="/posts' + query + '">' + (numPosts - 1) + ' children</a>';
1169 }
1170 else {
1171 if (numPosts === 1)
1172 msg = 'This post has no safe <a href="/posts' + query + '">children</a>';
1173 else if (numPosts === 2)
1174 msg = 'This post has <a href="/posts' + query + '">a child</a>';
1175 else if (numPosts > 2)
1176 msg = 'This post has <a href="/posts' + query + '">' + (numPosts - 1) + ' children</a>';
1177 }
1178 }
1179
1180 // Create the main notice element.
1181 childSpan.innerHTML = '<div class="ui-corner-all ui-state-highlight notice ' + classes + '"> ' + msg + ' (<a href="/wiki_pages?title=help%3Apost_relationships">learn more</a>) <a href="#" id="' + previewLinkId + '">' + previewLinkTxt + '</a> <div id="' + previewId + '" style="display: ' + displayStyle + ';"> </div> </div>';
1182
1183 var newNotice = childSpan.firstElementChild;
1184 var thumbDiv = getId(previewId, newNotice);
1185 var previewLink = getId(previewLinkId, newNotice);
1186
1187 // Create the thumbnails.
1188 for (i = numPosts - 1; i >= 0; i--) {
1189 post = posts[i];
1190
1191 if ((!show_loli && /(?:^|\s)loli(?:$|\s)/.test(post.tag_string)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(post.tag_string)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(post.tag_string)) || (!show_deleted && post.is_deleted && !forceShowDeleted) || (!show_banned && post.is_banned) || safebPostTest(post))
1192 continue;
1193
1194 var thumb = createThumbHTML(post, (clean_links ? "" : query)) + " ";
1195
1196 if (post.id === parentId)
1197 thumbs = thumb + thumbs;
1198 else
1199 thumbs += thumb;
1200 }
1201
1202 thumbDiv.innerHTML = thumbs;
1203
1204 // Highlight the post we're on.
1205 var activeThumb = getId("post_" + activePost.id, thumbDiv);
1206
1207 if (activeThumb)
1208 activeThumb.bbbAddClass("current-post");
1209
1210 // Make the show/hide links work.
1211 previewLink.bbbOverrideClick(function(event) {
1212 if (thumbDiv.style.display === "block") {
1213 thumbDiv.style.display = "none";
1214 previewLink.innerHTML = "show »";
1215 createCookie("show-relationship-previews", 0, 365);
1216 }
1217 else {
1218 thumbDiv.style.display = "block";
1219 previewLink.innerHTML = "« hide";
1220 createCookie("show-relationship-previews", 1, 365);
1221 }
1222
1223 event.preventDefault();
1224 });
1225
1226 // Prepare thumbnails.
1227 prepThumbnails(newNotice);
1228
1229 // Replace/add the notice.
1230 if (target)
1231 target.parentNode.replaceChild(newNotice, target);
1232 else if (mode === "child") {
1233 target = document.getElementsByClassName("notice-parent")[0] || bbb.el.resizeNotice || document.getElementById("image-container");
1234 target.parentNode.insertBefore(newNotice, target);
1235 }
1236 else if (mode === "parent") {
1237 target = bbb.el.resizeNotice || document.getElementById("image-container");
1238 target.parentNode.insertBefore(newNotice, target);
1239 }
1240 }
1241
1242 function endlessXMLJSONHandler(xml, optArg) {
1243 // Create a thumbnail listing from JSON results and pass it to the queue.
1244 var orderedIds = optArg;
1245 var posts = createThumbListing(xml, orderedIds);
1246 var newPage = document.createElement("div");
1247 newPage.className = "bbb-endless-page";
1248
1249 newPage.appendChild(posts);
1250 endlessQueuePage(newPage);
1251 }
1252
1253 /* Functions for XML page info */
1254 function searchPages(mode, optArg) {
1255 // Let other functions that don't require the API run (alternative to searchJSON) and retrieve various pages for info.
1256 var url; // If/else variable.
1257
1258 if (mode === "search" || mode === "favorites" || mode === "thumbnails") {
1259 url = updateURLQuery(location.href, {limit: thumbnail_count});
1260 bbb.flags.thumbs_xml = true;
1261
1262 fetchPages(url, "thumbnails");
1263 bbbStatus("posts", "new");
1264 }
1265 else if (mode === "endless") {
1266 url = endlessNexURL();
1267 bbb.flags.endless_xml = true;
1268
1269 fetchPages(url, "endless");
1270 bbbStatus("posts", "new");
1271 }
1272 else if (mode === "paginator") {
1273 url = (allowUserLimit() ? updateURLQuery(location.href, {limit: thumbnail_count}) : location.href);
1274 bbb.flags.paginator_xml = true;
1275
1276 fetchPages(url, "paginator");
1277 }
1278 else if (mode === "post_comments") {
1279 url = "/posts/" + optArg;
1280
1281 fetchPages(url, "post_comments", optArg);
1282 bbbStatus("post_comments", "new");
1283 }
1284 else if (mode === "account_check") {
1285 url = "/users/" + optArg + "/edit";
1286
1287 fetchPages(url, "account_check");
1288 }
1289 }
1290
1291 function fetchPages(url, mode, optArg, session, retries) {
1292 // Retrieve an actual page for certain pieces of information.
1293 var xmlhttp = new XMLHttpRequest();
1294 var xmlRetries = retries || 0;
1295 var xmlSession = session || bbb.session;
1296
1297 if (xmlhttp !== null) {
1298 xmlhttp.onreadystatechange = function() {
1299 if (xmlSession !== bbb.session) // If we end up receiving an xml response form a different page, reject it.
1300 xmlhttp.abort();
1301 else if (xmlhttp.readyState === 4) { // 4 = "loaded"
1302 if (xmlhttp.status === 200) { // 200 = "OK"
1303 var docEl = document.createElement("html");
1304
1305 docEl.innerHTML = xmlhttp.responseText;
1306
1307 if (mode === "paginator") {
1308 bbb.flags.paginator_xml = false;
1309
1310 replacePaginator(docEl);
1311 }
1312 else if (mode === "post_comments") {
1313 replaceComments(docEl, optArg);
1314 bbbStatus("post_comments", "done");
1315 }
1316 else if (mode === "thumbnails") {
1317 bbb.flags.thumbs_xml = false;
1318
1319 replaceThumbnails(docEl);
1320 bbbStatus("posts", "done");
1321 }
1322 else if (mode === "endless") {
1323 bbb.flags.endless_xml = false;
1324
1325 endlessXMLPageHandler(docEl);
1326 bbbStatus("posts", "done");
1327 }
1328 else if (mode === "account_check")
1329 accountUpdateHandler(docEl);
1330 }
1331 else if (!bbb.flags.unloading) {
1332 if (xmlhttp.status !== 0 && xmlRetries < 1) {
1333 xmlRetries++;
1334 fetchPages(url, mode, optArg, xmlSession, xmlRetries);
1335 }
1336 else {
1337 var linkId = uniqueIdNum(); // Create a unique ID.
1338 var msg; // If/else variable.
1339
1340 if (mode === "thumbnails" || mode === "endless") {
1341 msg = "Error retrieving post information";
1342 bbbStatus("posts", "error");
1343 }
1344 else if (mode === "post_comments") {
1345 msg = "Error retrieving comment information";
1346 bbbStatus("post_comments", "error");
1347 }
1348 else if (mode === "paginator")
1349 msg = "Error updating paginator";
1350 else if (mode === "account_check")
1351 msg = "Error checking account settings";
1352
1353 var noticeMsg = bbbNotice(msg + ' (HTML Code: ' + xmlhttp.status + ' ' + xmlhttp.statusText + '). ' + (xmlhttp.status === 0 ? 'The connection was unexpectedly cancelled or timed out. ' : '') + '(<a id="' + linkId + '" href="#">Retry</a>)', -1);
1354
1355 document.getElementById(linkId).addEventListener("click", function(event) {
1356 if (event.button !== 0)
1357 return;
1358
1359 closeBbbNoticeMsg(noticeMsg);
1360 searchPages(mode, optArg);
1361 event.preventDefault();
1362 }, false);
1363 }
1364 }
1365 }
1366 };
1367 xmlhttp.open("GET", url, true);
1368 xmlhttp.timeout = 120000;
1369 xmlhttp.send(null);
1370 }
1371 }
1372
1373 function replacePaginator(el) {
1374 // Replace the contents inside the paginator div so as to preserve the original div and any event listeners attached to it.
1375 var oldPag = getPaginator();
1376 var newPag = getPaginator(el);
1377
1378 if (oldPag && newPag)
1379 oldPag.innerHTML = newPag.innerHTML;
1380 }
1381
1382 function replaceComments(docEl, postId) {
1383 // Fix hidden comments with information from a post.
1384 var divId = "post_" + postId;
1385 var commentDiv = document.getElementById(divId);
1386 var commentSection = docEl.getElementsByClassName("comments-for-post")[0];
1387 var comments = commentSection.getElementsByClassName("comment");
1388 var numComments = comments.length;
1389 var toShow = 6; // Number of comments to display.
1390 var target = commentDiv.getElementsByClassName("comments-for-post")[0];
1391 var newContent = document.createDocumentFragment();
1392
1393 // Fix the comments.
1394 if (numComments > toShow) {
1395 for (var i = 0, toHide = numComments - toShow; i < toHide; i++)
1396 comments[i].style.display = "none";
1397
1398 commentSection.getElementsByClassName("row notices")[0].innerHTML = '<span class="info" id="threshold-comments-notice-for-' + postId + '"> <a href="/comments?include_below_threshold=true&post_id=' + postId + '" data-remote="true">Show all comments</a> </span>';
1399 }
1400
1401 // Add it all in and get it ready.
1402 while (commentSection.firstElementChild)
1403 newContent.appendChild(commentSection.firstElementChild);
1404
1405 target.appendChild(newContent);
1406
1407 Danbooru.Comment.initialize_all();
1408 $("#" + divId + " .simple_form .dtext-preview").hide();
1409 $("#" + divId + " .simple_form input[value=Preview]").click(Danbooru.Dtext.click_button);
1410 }
1411
1412 function replaceThumbnails(docEl) {
1413 // Replace the thumbnails and paginator with new ones.
1414 // Thumb preparation.
1415 var newThumbs = document.createDocumentFragment();
1416 var newPosts = getPosts(docEl);
1417
1418 for (var i = 0, il = newPosts.length; i < il; i++)
1419 newThumbs.appendChild(newPosts[i]);
1420
1421 // Update the existing thumbnails with new ones.
1422 updateThumbListing(newThumbs);
1423
1424 // Replace paginator with new paginator.
1425 replacePaginator(docEl);
1426
1427 // Update the URL with the limit value.
1428 fixURLLimit();
1429 }
1430
1431 function endlessXMLPageHandler(docEl) {
1432 // Take thumbnails from a page and pass them to the queue or retrieve hidden posts as necessary.
1433 bbb.endless.new_paginator = getPaginator(docEl);
1434
1435 if (useAPI() && potentialHiddenPosts(gLoc, docEl))
1436 searchJSON("endless");
1437 else {
1438 var posts = getPosts(docEl);
1439 var newPage = document.createElement("div");
1440 newPage.className = "bbb-endless-page";
1441
1442 for (var i = 0, il = posts.length; i < il; i++)
1443 newPage.appendChild(posts[i]);
1444
1445 endlessQueuePage(newPage);
1446 }
1447 }
1448
1449 function accountUpdateHandler(docEl) {
1450 // Check the user's settings page for changes and update BBB settings as needed.
1451 var perPageInput = docEl.querySelector("#user_per_page option[selected]");
1452 var perPage = Number((perPageInput ? perPageInput.value : 20));
1453
1454 if (thumbnail_count_default !== perPage) {
1455 updateSettings("thumbnail_count_default", perPage);
1456 bbbNotice("Your \"thumbnail count default\" BBB setting has been automatically changed to \"" + perPage + "\" to match your Danbooru account settings.", 0);
1457 }
1458
1459 createCookie("bbb_acc_check", 0, -1);
1460 }
1461
1462 /* Functions for retrieving page info */
1463 function scrapePost(docEl) {
1464 // Retrieve info from the current document or a supplied element containing the HTML with it and return it as API styled info.
1465 var target = docEl || document;
1466 var postContent = getPostContent(target);
1467 var imgContainer = postContent.container;
1468
1469 if (!imgContainer)
1470 return {};
1471
1472 var postEl = postContent.el;
1473 var postTag = (postEl ? postEl.tagName : undefined);
1474 var flags = imgContainer.getAttribute("data-flags") || "";
1475
1476 var imgInfo = {
1477 md5: imgContainer.getAttribute("data-md5") || "",
1478 file_ext: imgContainer.getAttribute("data-file-ext") || "",
1479 file_url: imgContainer.getAttribute("data-file-url") || "",
1480 large_file_url: imgContainer.getAttribute("data-large-file-url") || "",
1481 preview_file_url: imgContainer.getAttribute("data-preview-file-url") || "",
1482 has_large: undefined,
1483 id: Number(imgContainer.getAttribute("data-id")) || 0,
1484 pixiv_id: Number(imgContainer.getAttribute("data-pixiv-id")) || null,
1485 fav_count: Number(imgContainer.getAttribute("data-fav-count")) || 0,
1486 has_children: (imgContainer.getAttribute("data-has-children") === "true"),
1487 has_active_children: (postTag === "IMG" || postTag === "CANVAS" ? postEl.getAttribute("data-has-active-children") === "true" : !!target.getElementsByClassName("notice-parent")[0]),
1488 is_favorited: (imgContainer.getAttribute("data-is-favorited") === "true"),
1489 normalized_source: imgContainer.getAttribute("data-normalized-source") || "",
1490 parent_id: (imgContainer.getAttribute("data-parent-id") ? Number(imgContainer.getAttribute("data-parent-id")) : null),
1491 rating: imgContainer.getAttribute("data-rating") || "",
1492 score: Number(imgContainer.getAttribute("data-score")) || 0,
1493 source: imgContainer.getAttribute("data-source") || "",
1494 tag_string: imgContainer.getAttribute("data-tags") || "",
1495 tag_string_artist: scrapePostTags("artist", target),
1496 tag_string_character: scrapePostTags("character", target),
1497 tag_string_copyright: scrapePostTags("copyright", target),
1498 tag_string_general: scrapePostTags("general", target),
1499 tag_string_meta: scrapePostTags("meta", target),
1500 pool_string: imgContainer.getAttribute("data-pools") || "",
1501 uploader_name: imgContainer.getAttribute("data-uploader") || "",
1502 uploader_id: Number(imgContainer.getAttribute("data-uploader-id")) || 0,
1503 approver_id: imgContainer.getAttribute("data-approver-id") || null,
1504 is_deleted: (flags.indexOf("deleted") > -1),
1505 is_flagged: (flags.indexOf("flagged") > -1),
1506 is_pending: (flags.indexOf("pending") > -1),
1507 is_banned: (flags.indexOf("banned") > -1),
1508 image_height: Number(imgContainer.getAttribute("data-height")) || null,
1509 image_width: Number(imgContainer.getAttribute("data-width")) || null
1510 };
1511
1512 imgInfo.keeper_data = {uid: Number(imgContainer.getAttribute("data-top-tagger")) || imgInfo.uploader_id || 0};
1513 imgInfo.has_large = (imgInfo.large_file_url !== imgInfo.file_url);
1514
1515 // Grab any available Ugoira data.
1516 if (postTag === "CANVAS" || (imgInfo.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(imgInfo.tag_string))) {
1517 imgInfo.pixiv_ugoira_frame_data = {
1518 id: undefined, // Don't have this value.
1519 post_id: imgInfo.id,
1520 data: (postTag === "CANVAS" ? parseJson(postEl.getAttribute("data-ugoira-frames"), []) : ""),
1521 content_type: (postTag === "CANVAS" ? postEl.getAttribute("data-ugoira-content-type").replace(/"/gi, "") : "")
1522 };
1523 }
1524
1525 return formatInfo(imgInfo);
1526 }
1527
1528 function scrapePostTags(category, docEl) {
1529 // Retrieve tag info by type from the current document or a supplied element containing the HTML with it.
1530 var target = docEl || document;
1531 var tagList = getId("tag-list", target);
1532 var categoryClass; // If/else variable.
1533
1534 if (!tagList)
1535 return "";
1536
1537 if (category === "copyright")
1538 categoryClass = "category-3";
1539 else if (category === "character")
1540 categoryClass = "category-4";
1541 else if (category === "artist")
1542 categoryClass = "category-1";
1543 else if (category === "general")
1544 categoryClass = "category-0";
1545 else if (category === "meta")
1546 categoryClass = "category-5";
1547
1548 var categoryTags = tagList.getElementsByClassName(categoryClass);
1549 var categoryString = "";
1550
1551 for (var i = 0, il = categoryTags.length; i < il; i++) {
1552 var tagItem = categoryTags[i];
1553 var tag = tagItem.getElementsByClassName("search-tag")[0];
1554
1555 if (tag)
1556 categoryString += " " + tag.textContent.replace(/(\S)\s+(\S)/g, "$1_$2");
1557 }
1558
1559 return categoryString.bbbSpaceClean();
1560 }
1561
1562 function scrapeThumb(post) {
1563 // Retrieve info from a thumbnail and return it as API styled info. Mainly for remaking thumbnails.
1564 var flags = post.getAttribute("data-flags") || "";
1565 var imgInfo = {
1566 md5: post.getAttribute("data-md5") || "",
1567 file_ext: post.getAttribute("data-file-ext") || "",
1568 file_url: post.getAttribute("data-file-url") || "",
1569 large_file_url: post.getAttribute("data-large-file-url") || "",
1570 preview_file_url: post.getAttribute("data-preview-file-url") || "",
1571 file_url_desc: post.getAttribute("data-file-url-desc") || undefined,
1572 has_large: undefined,
1573 id: Number(post.getAttribute("data-id")) || 0,
1574 pixiv_id: Number(post.getAttribute("data-pixiv-id")) || null,
1575 fav_count: Number(post.getAttribute("data-fav-count")) || 0,
1576 has_children: (post.getAttribute("data-has-children") === "true"),
1577 has_active_children: post.bbbHasClass("post-status-has-children"), // Assumption. Basically a flag for the children class.
1578 is_favorited: (post.getAttribute("data-is-favorited") === "true"),
1579 normalized_source: post.getAttribute("data-normalized-source") || "",
1580 parent_id: (post.getAttribute("data-parent-id") ? Number(post.getAttribute("data-parent-id")) : null),
1581 rating: post.getAttribute("data-rating") || "",
1582 score: Number(post.getAttribute("data-score")) || 0,
1583 source: post.getAttribute("data-source") || "",
1584 tag_string: post.getAttribute("data-tags") || "",
1585 pool_string: post.getAttribute("data-pools") || "",
1586 uploader_name: post.getAttribute("data-uploader") || "",
1587 uploader_id: Number(post.getAttribute("data-uploader-id")) || 0,
1588 approver_id: post.getAttribute("data-approver-id") || null,
1589 is_deleted: (flags.indexOf("deleted") > -1),
1590 is_flagged: (flags.indexOf("flagged") > -1),
1591 is_pending: (flags.indexOf("pending") > -1),
1592 is_banned: (flags.indexOf("banned") > -1),
1593 image_height: Number(post.getAttribute("data-height")) || null,
1594 image_width: Number(post.getAttribute("data-width")) || null
1595 };
1596
1597 imgInfo.keeper_data = {uid: Number(post.getAttribute("data-top-tagger")) || imgInfo.uploader_id || 0};
1598
1599 if (imgInfo.file_url)
1600 imgInfo.has_large = (imgInfo.file_url !== imgInfo.large_file_url);
1601 else {
1602 var isUgoira = /(?:^|\s)ugoira(?:$|\s)/.test(imgInfo.tag_string);
1603 var isAnimatedImg = /(?:^|\s)animated_(?:gif|png)(?:$|\s)/.test(imgInfo.tag_string);
1604 var isVideo = /(?:^|\s)(?:webm|mp4)(?:$|\s)/.test(imgInfo.tag_string);
1605 var isFlash = /(?:^|\s)flash(?:$|\s)/.test(imgInfo.tag_string);
1606 var isBigImg = (imgInfo.image_width > 850 && !isFlash && !isVideo);
1607
1608 imgInfo.has_large = (!isAnimatedImg && (isBigImg || isUgoira));
1609 }
1610
1611 return formatInfo(imgInfo);
1612 }
1613
1614 function getId(elId, target) {
1615 // Retrieve an element by ID from either the current document or an element containing it.
1616 if (!target || target === document)
1617 return document.getElementById(elId);
1618 else if (target.id === elId)
1619 return target;
1620 else
1621 return target.querySelector("#" + elId);
1622
1623 return null;
1624 }
1625
1626 function getPostContent(docEl) {
1627 // Retrieve the post content related elements.
1628 var target = docEl || document;
1629 var imgContainer = getId("image-container", target);
1630
1631 if (!imgContainer)
1632 return {};
1633
1634 var img = getId("image", target);
1635 var swfObj = imgContainer.getElementsByTagName("object")[0];
1636 var swfEmb = (swfObj ? swfObj.getElementsByTagName("embed")[0] : undefined);
1637 var video = imgContainer.getElementsByTagName("video")[0];
1638 var ugoira = imgContainer.getElementsByTagName("canvas")[0];
1639 var other = imgContainer.querySelector("a[href^='/data/']");
1640 var el = swfEmb || video || ugoira || img || other;
1641 var secondaryEl = swfObj; // Other elements related to the main element. Only applies to flash for now.
1642
1643 return {container: imgContainer, el: el, secEl: secondaryEl};
1644 }
1645
1646 function getPosts(target) {
1647 // Return a list of posts depending from the document or a specific element.
1648 if (!target || target === document) // All posts in the document.
1649 return document.querySelectorAll(".post-preview");
1650 else if (target instanceof DocumentFragment || !target.bbbHasClass("post-preview")) // All posts in a specific element.
1651 return target.querySelectorAll(".post-preview");
1652 else // Single specific post.
1653 return [target];
1654 }
1655
1656 function getPaginator(target) {
1657 // Return the paginator of the document or a specific element.
1658 if (!target || target === document) // Paginator in the document.
1659 return document.getElementsByClassName("paginator")[0];
1660 else if (!target.bbbHasClass("paginator")) // Paginator in a specific element.
1661 return target.getElementsByClassName("paginator")[0];
1662 else // Single specific paginator.
1663 return target;
1664 }
1665
1666 function getThumbContainer(mode, docEl) {
1667 // Retrieve the element that contains the thumbnails.
1668 var target = docEl || document;
1669 var container; // If/else variable.
1670
1671 if (mode === "search")
1672 container = getId("posts-container", target);
1673 else if (mode === "popular" || mode === "popular_view")
1674 container = getId("a-popular", target);
1675 else if (mode === "pool" || mode === "favorite_group") {
1676 container = getId("a-show", target);
1677 container = (container ? container.getElementsByTagName("section")[1] : undefined);
1678 }
1679 else if (mode === "favorites")
1680 container = getId("posts", target);
1681
1682 // Can't always depend on the first post so it's used as a fallback.
1683 if (!container) {
1684 var firstPost = getPosts(target)[0];
1685
1686 if (firstPost)
1687 container = firstPost.parentNode;
1688 }
1689
1690 return container;
1691 }
1692
1693 function getThumbSibling(mode, docEl) {
1694 // If it exists, retrieve the element that thumbnails should be added before.
1695 var target = docEl || document;
1696 var sibling; // If/else variable.
1697
1698 var posts = getPosts(target);
1699 var numPosts = posts.length;
1700 var lastPost = (numPosts ? posts[numPosts - 1] : undefined);
1701 var lastPostParent = (lastPost ? lastPost.parentNode : undefined);
1702 var thumbContainer = getThumbContainer(mode, target);
1703 var lastPostEl = (lastPostParent && lastPostParent !== thumbContainer && lastPostParent.parentNode === thumbContainer ? lastPostParent : lastPost);
1704
1705 if (lastPostEl) {
1706 var contChildren = thumbContainer.children;
1707
1708 for (var i = contChildren.length - 1; i >= 0; i--) {
1709 if (contChildren[i] === lastPostEl) {
1710 sibling = contChildren[i + 1];
1711 break;
1712 }
1713 }
1714 }
1715 else if (mode === "pool" || mode === "favorites" || mode === "favorite_group") {
1716 var paginator = getPaginator(target);
1717 var endlessDiv = getId("bbb-endless-button-div", target);
1718
1719 sibling = endlessDiv || paginator;
1720 }
1721
1722 return sibling;
1723 }
1724
1725 function getPaginatorNextURL(target) {
1726 // Retrieve the next page's URL from the paginator.
1727 var paginator = getPaginator(target);
1728
1729 if (paginator) {
1730 var nextLink = paginator.querySelector("a[rel='next'][href]");
1731
1732 if (nextLink)
1733 return nextLink.href;
1734 }
1735
1736 return undefined;
1737 }
1738
1739 function getMeta(meta, docEl) {
1740 // Get a value from an HTML meta tag.
1741 var target = docEl || document;
1742 var metaTag = target.querySelector("meta[name='" + meta + "'][content], meta[property='" + meta + "'][content]");
1743
1744 if (metaTag)
1745 return metaTag.content;
1746 else
1747 return undefined;
1748 }
1749
1750 function getUserData(name) {
1751 // Get user account data from the body data attributes.
1752 var value = document.body.getAttribute("data-user-" + name);
1753
1754 if (bbbIsNum(value))
1755 value = Number(value);
1756
1757 return value;
1758 }
1759
1760 function getVar(urlVar, targetUrl) {
1761 // Retrieve a value from a specified/current URL's query string.
1762 // Undefined refers to a param that isn't even declared. Null refers to a declared param that hasn't been defined with a value (&test&). An empty string ("") refers to a param that has been defined with nothing (&test=&).
1763 var url = targetUrl;
1764
1765 if (!url)
1766 url = location.search;
1767
1768 var result = url.split(new RegExp("[&\?]" + urlVar))[1];
1769
1770 if (result === undefined)
1771 return undefined;
1772
1773 result = result.split(/[#&]/, 1)[0].split("=", 2)[1];
1774
1775 if (result === undefined)
1776 return null;
1777 else
1778 return result;
1779 }
1780
1781 function getTagVar(urlVar, url) {
1782 // Retrieve a metatag's value from the tag portion of a specified/current URL's query string.
1783 if (!url)
1784 url = location.search;
1785
1786 var tags = getVar("tags", url);
1787
1788 // If the tags parameter isn't provided or has no value, the metatag is undefined.
1789 if (tags === null || tags === undefined)
1790 return undefined;
1791
1792 tags = tags.split(/\+|%20/g);
1793
1794 for (var i = 0, il = tags.length; i < il; i++) {
1795 var tag = decodeURIComponent(tags[i]);
1796
1797 if (tag.indexOf(urlVar + ":") === 0)
1798 return encodeURIComponent(tag.split(":")[1]); // Let the calling function decide whether it wants the decoded tag or not.
1799 }
1800
1801 return undefined;
1802 }
1803
1804 function getThumbQuery() {
1805 // Return the thumbnail URL query value.
1806 var query = "";
1807
1808 if (gLoc === "search" || gLoc === "favorites") {
1809 query = getCurTags();
1810 query = (query ? "?tags=" + query : "");
1811 }
1812 else if (gLoc === "pool")
1813 query = "?pool_id=" + location.pathname.match(/\/pools\/(\d+)/)[1];
1814 else if (gLoc === "favorite_group")
1815 query = "?favgroup_id=" + location.pathname.match(/\/favorite_groups\/(\d+)/)[1];
1816
1817 return query;
1818 }
1819
1820 function getCurTags() {
1821 // Retrieve the current search tags for URL use.
1822 var tags; // If/else variable.
1823
1824 if (gLoc === "search")
1825 tags = getVar("tags") || "";
1826 else if (gLoc === "favorites") {
1827 tags = document.getElementById("tags");
1828 tags = (tags ? tags.getAttribute("value").replace("fav:", "ordfav:").bbbSpaceClean() : ""); // Use getAttribute to avoid potential user changes to the input.
1829 }
1830
1831 return tags;
1832 }
1833
1834 function getLimit(url) {
1835 // Retrieve the current specified limit value. The query limit overrides the search limit.
1836 var loc = danbLoc(url);
1837 var limit; // If/else variable.
1838
1839 if (loc === "pool" || loc === "popular" || loc === "favorite_group")
1840 limit = thumbnail_count_default;
1841 else if (loc === "comments")
1842 limit = 5;
1843 else if (loc === "popular_view")
1844 limit = 101;
1845 else {
1846 var queryLimit = getQueryLimit(url);
1847 var searchLimit = getSearchLimit(url);
1848
1849 limit = (queryLimit !== undefined ? queryLimit : searchLimit);
1850 }
1851
1852 return limit;
1853 }
1854
1855 function getQueryLimit(url) {
1856 // Retrieve the limit from a URL's query portion. Always use the default for certain areas where the limit is not allowed.
1857 var queryLimit = getVar("limit", url);
1858
1859 if (queryLimit !== null && queryLimit !== undefined) { // Treat the limit as undefined when the limit parameter is declared with no value.
1860 queryLimit = decodeURIComponent(queryLimit);
1861
1862 if (queryLimit === "" || !/^\s*\d+/.test(queryLimit)) // No thumbnails show up when the limit is declared with a blank value or has no number directly after any potential white space.
1863 return 0;
1864 else // The query limit finds its value in a manner similar to parseInt. Dump leading spaces and grab numbers until a non-numerical character is hit.
1865 return parseInt(queryLimit, 10);
1866 }
1867
1868 return undefined;
1869 }
1870
1871 function getSearchLimit(url) {
1872 // Retrieve the limit from the search/limit tag used in a search.
1873 var searchLimit = getTagVar("limit", url);
1874
1875 if (searchLimit !== undefined) {
1876 searchLimit = decodeURIComponent(searchLimit);
1877
1878 if (searchLimit === "") // No thumbnails show up when the limit is declared but left blank.
1879 return 0;
1880 else if (!bbbIsNum(searchLimit.replace(/\s/g, "")) || searchLimit.indexOf(".") > -1 || Number(searchLimit) < 0) // Non-numerical, negative, and decimal values are ignored. Treat the limit as undefined.
1881 return undefined;
1882 else
1883 return Number(searchLimit);
1884 }
1885
1886 return undefined;
1887 }
1888
1889 /* Functions for the settings panel */
1890 function injectSettings() {
1891 var menu = document.getElementById("top");
1892 menu = (menu ? menu.getElementsByTagName("menu")[0] : undefined);
1893
1894 if (!menu)
1895 return;
1896
1897 var menuItems = menu.getElementsByTagName("li");
1898 var numMenuItems = menu.getElementsByTagName("li").length;
1899 var moreItem = menuItems[numMenuItems - 1];
1900
1901 for (var i = numMenuItems - 1; i >= 0; i--) {
1902 var menuLink = menuItems[i];
1903
1904 if (menuLink.textContent.indexOf("More") > -1) {
1905 moreItem = menuLink;
1906 break;
1907 }
1908 }
1909
1910 var link = document.createElement("a");
1911 link.href = "#";
1912 link.innerHTML = "BBB Settings";
1913 link.addEventListener("click", function(event) {
1914 if (event.button !== 0)
1915 return;
1916
1917 openMenu();
1918 event.preventDefault();
1919 }, false);
1920
1921 var item = document.createElement("li");
1922 item.appendChild(link);
1923
1924 if (moreItem)
1925 menu.insertBefore(item, moreItem);
1926 else
1927 menu.appendChild(item);
1928
1929 window.addEventListener("resize", adjustMenuTimer, false);
1930 }
1931
1932 function openMenu() {
1933 if (bbb.el.menu.window)
1934 return;
1935
1936 loadSettings();
1937 createMenu();
1938 }
1939
1940 function reloadMenu() {
1941 removeMenu();
1942 createMenu();
1943 }
1944
1945 function createMenu() {
1946 var menu = bbb.el.menu.window = document.createElement("div");
1947 menu.id = "bbb-menu";
1948 menu.style.visibility = "hidden";
1949
1950 var tip = bbb.el.menu.tip = document.createElement("div");
1951 tip.id = "bbb-expl";
1952 menu.appendChild(tip);
1953
1954 var header = document.createElement("h1");
1955 header.innerHTML = "Better Better Booru Settings";
1956 header.style.textAlign = "center";
1957 menu.appendChild(header);
1958
1959 var tabBar = document.createElement("div");
1960 tabBar.style.padding = "0px 15px";
1961 tabBar.addEventListener("click", function(event) {
1962 if (event.button !== 0)
1963 return;
1964
1965 var target = event.target;
1966
1967 if (target.href)
1968 changeTab(target);
1969
1970 event.preventDefault();
1971 }, false);
1972 menu.appendChild(tabBar);
1973
1974 var generalTab = bbb.el.menu.generalTab = document.createElement("a");
1975 generalTab.name = "general";
1976 generalTab.href = "#";
1977 generalTab.innerHTML = "General";
1978 generalTab.className = "bbb-tab bbb-active-tab";
1979 tabBar.appendChild(generalTab);
1980
1981 var blacklistTab = bbb.el.menu.blacklistTab = document.createElement("a");
1982 blacklistTab.name = "blacklist";
1983 blacklistTab.href = "#";
1984 blacklistTab.innerHTML = "Blacklist";
1985 blacklistTab.className = "bbb-tab";
1986 tabBar.appendChild(blacklistTab);
1987
1988 var borderTab = bbb.el.menu.borderTab = document.createElement("a");
1989 borderTab.name = "borders";
1990 borderTab.href = "#";
1991 borderTab.innerHTML = "Borders";
1992 borderTab.className = "bbb-tab";
1993 tabBar.appendChild(borderTab);
1994
1995 var groupTab = bbb.el.menu.groupTab = document.createElement("a");
1996 groupTab.name = "groups";
1997 groupTab.href = "#";
1998 groupTab.innerHTML = "Groups";
1999 groupTab.className = "bbb-tab";
2000 tabBar.appendChild(groupTab);
2001
2002 var layoutTab = bbb.el.menu.layoutTab = document.createElement("a");
2003 layoutTab.name = "layout";
2004 layoutTab.href = "#";
2005 layoutTab.innerHTML = "Layout";
2006 layoutTab.className = "bbb-tab";
2007 tabBar.appendChild(layoutTab);
2008
2009 var prefTab = bbb.el.menu.prefTab = document.createElement("a");
2010 prefTab.name = "pref";
2011 prefTab.href = "#";
2012 prefTab.innerHTML = "Preferences";
2013 prefTab.className = "bbb-tab";
2014 tabBar.appendChild(prefTab);
2015
2016 var helpTab = bbb.el.menu.helpTab = document.createElement("a");
2017 helpTab.name = "help";
2018 helpTab.href = "#";
2019 helpTab.innerHTML = "Help";
2020 helpTab.className = "bbb-tab";
2021 tabBar.appendChild(helpTab);
2022
2023 var scrollDiv = bbb.el.menu.scrollDiv = document.createElement("div");
2024 scrollDiv.className = "bbb-scroll-div";
2025 menu.appendChild(scrollDiv);
2026 scrollDiv.scrollTop = 0;
2027
2028 var generalPage = bbb.el.menu.generalPage = document.createElement("div");
2029 generalPage.className = "bbb-page";
2030 generalPage.style.display = "block";
2031 scrollDiv.appendChild(generalPage);
2032
2033 generalPage.bbbSection(bbb.sections.browse);
2034 generalPage.bbbSection(bbb.sections.control);
2035 generalPage.bbbSection(bbb.sections.endless);
2036 generalPage.bbbSection(bbb.sections.misc);
2037
2038 var blacklistPage = bbb.el.menu.blacklistPage = document.createElement("div");
2039 blacklistPage.className = "bbb-page";
2040 scrollDiv.appendChild(blacklistPage);
2041
2042 blacklistPage.bbbSection(bbb.sections.blacklist_options);
2043 blacklistPage.bbbBlacklistSection();
2044
2045 var layoutPage = bbb.el.menu.layoutPage = document.createElement("div");
2046 layoutPage.className = "bbb-page";
2047 scrollDiv.appendChild(layoutPage);
2048
2049 layoutPage.bbbSection(bbb.sections.sidebar);
2050 layoutPage.bbbSection(bbb.sections.notices);
2051 layoutPage.bbbSection(bbb.sections.misc_layout);
2052
2053 var bordersPage = bbb.el.menu.bordersPage = document.createElement("div");
2054 bordersPage.className = "bbb-page";
2055 scrollDiv.appendChild(bordersPage);
2056
2057 bordersPage.bbbSection(bbb.sections.border_options);
2058 bordersPage.bbbSection(bbb.sections.status_borders);
2059 bordersPage.bbbSection(bbb.sections.tag_borders);
2060
2061 var groupsPage = bbb.el.menu.groupsPage = document.createElement("div");
2062 groupsPage.className = "bbb-page";
2063 scrollDiv.appendChild(groupsPage);
2064
2065 groupsPage.bbbSection(bbb.sections.groups);
2066
2067 var prefPage = bbb.el.menu.prefPage = document.createElement("div");
2068 prefPage.className = "bbb-page";
2069 scrollDiv.appendChild(prefPage);
2070
2071 prefPage.bbbSection(bbb.sections.script_settings);
2072 prefPage.bbbBackupSection();
2073 prefPage.bbbEraseSection();
2074
2075 var helpPage = bbb.el.menu.helpPage = document.createElement("div");
2076 helpPage.className = "bbb-page";
2077 scrollDiv.appendChild(helpPage);
2078
2079 helpPage.bbbTextSection('Thumbnail Matching Rules', 'For creating thumbnail matching rules, please consult the following examples:<ul><li><b>tag1</b> - Match posts with tag1.</li><li><b>tag1 tag2</b> - Match posts with tag1 AND tag2.</li><li><b>-tag1</b> - Match posts without tag1.</li><li><b>tag1 -tag2</b> - Match posts with tag1 AND without tag2.</li><li><b>~tag1 ~tag2</b> - Match posts with tag1 OR tag2.</li><li><b>~tag1 ~-tag2</b> - Match posts with tag1 OR without tag2.</li><li><b>tag1 ~tag2 ~tag3</b> - Match posts with tag1 AND either tag2 OR tag3.</li></ul><br>Wildcards can be used with any of the above methods:<ul><li><b>~tag1* ~-*tag2</b> - Match posts with tags starting with tag1 or posts without tags ending with tag2.</li></ul><br>Multiple match rules can be specified by using commas or separate lines when possible:<ul><li><b>tag1 tag2, tag3 tag4</b> - Match posts with tag1 AND tag2 or posts with tag3 AND tag4.</li><li><b>tag1 ~tag2 ~tag3, tag4</b> - Match posts with tag1 AND either tag2 OR tag3 or posts with tag4.</li></ul><br>Tags can be nested/grouped together by using parentheses that only have spaces or commas next to them:<ul><li><b>( ~tag1 ~tag2 ) ( ~tag3 ~tag3 )</b> - Match posts with either tag1 OR tag2 AND either tag3 OR tag4.</li><li><b>tag1 ( tag2, tag3 tag4 )</b> - Match posts with tag1 AND tag2 or posts with tag1 AND tag3 AND tag4.</li><li><b>tag1 -( tag2 tag3 )</b> - Match posts with tag1 AND without tag2 AND tag3.</li><li><b>tag1 ~tag2 ~( tag3 tag4 )</b> - Match posts with tag1 and either tag2 OR tag3 AND tag4.</li></ul><br>The following metatags are supported:<ul><li><b>rating:safe</b> - Match posts rated safe. Accepted values include safe, explicit, and questionable.</li><li><b>status:pending</b> - Match pending posts. Accepted values include active, pending, flagged, banned, and deleted. Note that flagged posts also count as active posts.</li><li><b>user:albert</b> - Match posts made by the user Albert. Note that this tag will only work if you have a <b>moderator</b> level account or higher.</li><li><b>userid:1</b> - Match posts made by the user with an ID number of 1.</li><li><b>taggerid:1</b> - Match posts mostly tagged by the user with an ID number of 1.</li><li><b>approverid:1</b> - Match posts approved by the user with an ID number of 1. Accepted values include numbers, "any" for all posts with an approver, and "none" for posts without an approver.</li><li><b>source:http://www.4chan.org/</b> - Match posts with a source starting with http://www.4chan.org/. Accepted values include "any" for all posts with sources, "none" for all posts without sources, wildcard searches such as "*pixiv.net*" for posts with sources that contain pixiv.net, and non-wildcard searches that start matching at the beginning of a source.</li><li><b>isfav:true</b> - Match posts favorited under your current account. Accepted values include true and false.</li><li><b>filetype:jpg</b> - Match posts that are in the jpg format. Accepted values include jpg, png, gif, swf, zip, webm, and mp4.</li><li><b>group:hidden</b> or <b>g:hidden</b> - Match posts that match the tags in your group named \"hidden\".</li><li><b>pool:1</b> - Match posts that are in the pool with an ID number of 1. Accepted values include pool ID numbers, "series" for posts in series category pools, "collection" for posts in collection category pools, "any" for posts in any pool, "none" for posts not in a pool, "active" for posts in an active (not deleted) pool, and "inactive" for posts only in an inactive (deleted) pool.</li><li><b>parent:1</b> - Match posts that have the post with an ID number of 1 as a parent. Accepted values include post ID numbers, "any" for any posts with a parent, and "none" for posts without a parent.</li><li><b>child:any</b> - Match any posts that have children. Accepted values include "any" for any posts with children and "none" for posts without children.</li><li><b>id:1</b> - Match posts with an ID number of 1.</li><li><b>score:1</b> - Match posts with a score of 1.</li><li><b>favcount:1</b> - Match posts with a favorite count of 1.</li><li><b>height:1</b> - Match posts with a height of 1.</li><li><b>width:1</b> - Match posts with a width of 1.</li></ul><br>The id, score, favcount, width, and height metatags can also use number ranges for matching:<ul><li><b>score:<5</b> - Match posts with a score less than 5.</li><li><b>score:>5</b> - Match posts with a score greater than 5.</li><li><b>score:<=5</b> or <b>score:..5</b> - Match posts with a score equal to OR less than 5.</li><li><b>score:>=5</b> or <b>score:5..</b> - Match posts with a score equal to OR greater than 5.</li><li><b>score:1..5</b> - Match posts with a score equal to OR greater than 1 AND equal to OR less than 5.</li></ul>');
2080 helpPage.bbbTextSection('Hotkeys', '<b>Posts</b><ul><li><b>B</b> - Open BBB menu.</li><li><b>1</b> - Resize to window.</li><li><b>2</b> - Resize to window width.</li><li><b>3</b> - Resize to window height.</li><li><b>4</b> - Reset/remove resizing.</li></ul><div style="font-size: smaller;">Note: Numbers refer to the main typing keypad and not the numeric keypad.</div><br><b>General</b><ul><li><b>B</b> - Open BBB menu.</li><li><b>E</b> - Toggle endless pages.</li><li><b>Shift + E</b> - Continue loading more pages after pausing during endless pages.</li><li><b>F</b> - Open quick search.</li><li><b>Shift + F</b> - Reset quick search.</li></ul>');
2081 helpPage.bbbTextSection('Questions, Suggestions, or Bugs?', 'If you have any questions, please use the Greasy Fork feedback forums located <a target="_blank" href="https://greasyfork.org/scripts/3575-better-better-booru/feedback">here</a>. If you\'d like to report a bug or make a suggestion, please create an issue on GitHub <a target="_blank" href="https://github.com/pseudonymous/better-better-booru/issues">here</a>.');
2082 helpPage.bbbTocSection();
2083
2084 var close = document.createElement("a");
2085 close.innerHTML = "Save & Close";
2086 close.href = "#";
2087 close.className = "bbb-button";
2088 close.style.marginRight = "15px";
2089 close.addEventListener("click", function(event) {
2090 if (event.button !== 0)
2091 return;
2092
2093 removeMenu();
2094 saveSettings();
2095 event.preventDefault();
2096 }, false);
2097
2098 var cancel = document.createElement("a");
2099 cancel.innerHTML = "Cancel";
2100 cancel.href = "#";
2101 cancel.className = "bbb-button";
2102 cancel.addEventListener("click", function(event) {
2103 if (event.button !== 0)
2104 return;
2105
2106 removeMenu();
2107 loadSettings();
2108 event.preventDefault();
2109 }, false);
2110
2111 var reset = document.createElement("a");
2112 reset.innerHTML = "Reset to Defaults";
2113 reset.href = "#";
2114 reset.className = "bbb-button";
2115 reset.style.cssFloat = "right";
2116 reset.style.color = "#ff1100";
2117 reset.addEventListener("click", function(event) {
2118 if (event.button !== 0)
2119 return;
2120
2121 loadDefaults();
2122 reloadMenu();
2123 event.preventDefault();
2124 }, false);
2125
2126 menu.appendChild(close);
2127 menu.appendChild(cancel);
2128 menu.appendChild(reset);
2129
2130 // Add menu to the DOM and manipulate the dimensions.
2131 document.body.appendChild(menu);
2132
2133 var viewHeight = document.documentElement.clientHeight;
2134 var barWidth = scrollbarWidth();
2135 var scrollDivDiff = menu.offsetHeight - scrollDiv.clientHeight;
2136
2137 scrollDiv.style.maxHeight = viewHeight - scrollDiv.bbbGetPadding().height - scrollDivDiff - 50 + "px"; // Subtract 50 for margins (25 each).
2138 scrollDiv.style.minWidth = 901 + barWidth + 3 + "px"; // Should keep the potential scrollbar from intruding on the original drawn layout if I'm thinking about this correctly. Seems to work in practice anyway.
2139 scrollDiv.style.paddingLeft = barWidth + 3 + "px";
2140
2141 var menuWidth = menu.offsetWidth;
2142
2143 menu.style.marginLeft = -menuWidth / 2 + "px";
2144 menu.style.visibility = "visible";
2145 }
2146
2147 function createSection(section) {
2148 var sectionFrag = document.createDocumentFragment();
2149 var i, il; // Loop variables.
2150
2151 if (section.header) {
2152 var sectionHeader = document.createElement("h2");
2153 sectionHeader.innerHTML = section.header;
2154 sectionHeader.className = "bbb-header";
2155 sectionFrag.appendChild(sectionHeader);
2156 }
2157
2158 if (section.text) {
2159 var sectionText = document.createElement("div");
2160 sectionText.innerHTML = section.text;
2161 sectionText.className = "bbb-section-text";
2162 sectionFrag.appendChild(sectionText);
2163 }
2164
2165 var sectionDiv = document.createElement("div");
2166 sectionDiv.className = "bbb-section-options";
2167 sectionFrag.appendChild(sectionDiv);
2168
2169 if (section.type === "general") {
2170 var settingList = section.settings;
2171 var sll = settingList.length;
2172 var halfway = (sll > 1 ? Math.ceil(sll / 2) : 0);
2173
2174 var leftSide = document.createElement("div");
2175 leftSide.className = "bbb-section-options-left";
2176 sectionDiv.appendChild(leftSide);
2177
2178 var rightSide = document.createElement("div");
2179 rightSide.className = "bbb-section-options-right";
2180 sectionDiv.appendChild(rightSide);
2181
2182 var optionTarget = leftSide;
2183
2184 for (i = 0; i < sll; i++) {
2185 var settingName = settingList[i];
2186
2187 if (halfway && i >= halfway)
2188 optionTarget = rightSide;
2189
2190 var newOption = createOption(settingName);
2191 optionTarget.appendChild(newOption);
2192 }
2193 }
2194 else if (section.type === "border" || section.type === "group") {
2195 var listSettings = bbb.user[section.settings];
2196
2197 for (i = 0, il = listSettings.length; i < il; i++) {
2198 var newListOption = (section.type === "border" ? createBorderOption(listSettings, i) : createGroupOption(listSettings, i));
2199 sectionDiv.appendChild(newListOption);
2200 }
2201
2202 var indexWrapper = document.createElement("div");
2203 indexWrapper.bbbInfo("bbb-index", i);
2204 sectionDiv.appendChild(indexWrapper);
2205
2206 var listDivider = document.createElement("div");
2207 listDivider.className = "bbb-list-divider";
2208 indexWrapper.appendChild(listDivider);
2209 }
2210
2211 return sectionFrag;
2212 }
2213
2214 Element.prototype.bbbSection = function(section) {
2215 this.appendChild(createSection(section));
2216 };
2217
2218 function createOption(settingName) {
2219 var optionObject = bbb.options[settingName];
2220 var userSetting = bbb.user[settingName];
2221 var i, il; // Loop variables.
2222
2223 var label = document.createElement("label");
2224 label.className = "bbb-general-label";
2225
2226 var textSpan = document.createElement("span");
2227 textSpan.className = "bbb-general-text";
2228 textSpan.innerHTML = optionObject.label;
2229 label.appendChild(textSpan);
2230
2231 var inputSpan = document.createElement("span");
2232 inputSpan.className = "bbb-general-input";
2233 label.appendChild(inputSpan);
2234
2235 var item; // Switch variable.
2236 var itemFrag = document.createDocumentFragment();
2237
2238 switch (optionObject.type) {
2239 case "dropdown":
2240 var txtOptions = optionObject.txtOptions;
2241 var numRange = optionObject.numRange;
2242 var numList = optionObject.numList;
2243 var selectOption; // If/else variable.
2244
2245 item = document.createElement("select");
2246 item.name = settingName;
2247
2248 if (txtOptions) {
2249 for (i = 0, il = txtOptions.length; i < il; i++) {
2250 var txtOption = txtOptions[i].split(":");
2251
2252 selectOption = document.createElement("option");
2253 selectOption.innerHTML = txtOption[0];
2254 selectOption.value = txtOption[1];
2255
2256 if (selectOption.value === String(userSetting))
2257 selectOption.selected = true;
2258
2259 item.appendChild(selectOption);
2260 }
2261 }
2262
2263 if (numList) {
2264 for (i = 0, il = numList.length; i < il; i++) {
2265 selectOption = document.createElement("option");
2266 selectOption.innerHTML = numList[i];
2267 selectOption.value = numList[i];
2268
2269 if (selectOption.value === String(userSetting))
2270 selectOption.selected = true;
2271
2272 item.appendChild(selectOption);
2273 }
2274 }
2275
2276 if (numRange) {
2277 var end = numRange[1];
2278
2279 for (i = numRange[0]; i <= end; i++) {
2280 selectOption = document.createElement("option");
2281 selectOption.innerHTML = i;
2282 selectOption.value = i;
2283
2284 if (selectOption.value === String(userSetting))
2285 selectOption.selected = true;
2286
2287 item.appendChild(selectOption);
2288 }
2289 }
2290
2291 item.addEventListener("change", function() {
2292 var selected = this.value;
2293 bbb.user[settingName] = (bbbIsNum(selected) ? Number(selected) : selected);
2294 bbb.settings.changed[settingName] = true;
2295 }, false);
2296 itemFrag.appendChild(item);
2297 break;
2298 case "checkbox":
2299 item = document.createElement("input");
2300 item.name = settingName;
2301 item.type = "checkbox";
2302 item.checked = userSetting;
2303 item.addEventListener("click", function(event) {
2304 if (event.button !== 0)
2305 return;
2306
2307 bbb.user[settingName] = this.checked;
2308 bbb.settings.changed[settingName] = true;
2309 }, false);
2310 itemFrag.appendChild(item);
2311 break;
2312 case "text":
2313 item = document.createElement("input");
2314 item.name = settingName;
2315 item.type = "text";
2316 item.value = userSetting;
2317 item.addEventListener("change", function() {
2318 bbb.user[settingName] = (optionObject.isTagInput ? this.value.bbbTagClean() : this.value.bbbSpaceClean());
2319 bbb.settings.changed[settingName] = true;
2320 }, false);
2321 itemFrag.appendChild(item);
2322
2323 if (optionObject.isTagInput) {
2324 var tagExpand = document.createElement("a");
2325 tagExpand.href = "#";
2326 tagExpand.className = "bbb-edit-link";
2327 tagExpand.innerHTML = "»";
2328 tagExpand.addEventListener("click", function(event) {
2329 if (event.button !== 0)
2330 return;
2331
2332 tagEditWindow(item, bbb.user, settingName);
2333 event.preventDefault();
2334 }, false);
2335 itemFrag.appendChild(tagExpand);
2336 }
2337 break;
2338 case "number":
2339 item = document.createElement("input");
2340 item.name = settingName;
2341 item.type = "text";
2342 item.value = userSetting;
2343 item.addEventListener("change", function() {
2344 bbb.user[settingName] = Number(this.value);
2345 bbb.settings.changed[settingName] = true;
2346 }, false);
2347 itemFrag.appendChild(item);
2348 break;
2349 default:
2350 bbbNotice('Unexpected menu object type for "' + optionObject.label + '". (Type: ' + optionObject.type + ')', -1);
2351 return label;
2352 }
2353 inputSpan.appendChild(itemFrag);
2354
2355 var explLink = document.createElement("a");
2356 explLink.innerHTML = "?";
2357 explLink.href = "#";
2358 explLink.className = "bbb-expl-link";
2359 explLink.bbbSetTip(bbb.options[settingName].expl);
2360 inputSpan.appendChild(explLink);
2361
2362 return label;
2363 }
2364
2365 function createBorderOption(borderSettings, index) {
2366 var borderItem = borderSettings[index];
2367 var isStatus = (borderItem.class_name ? true : false);
2368
2369 var borderSpacer = document.createElement("span");
2370 borderSpacer.className = "bbb-list-spacer";
2371
2372 var indexWrapper = document.createElement("div");
2373 indexWrapper.bbbInfo("bbb-index", index);
2374
2375 var borderDivider = document.createElement("div");
2376 borderDivider.className = "bbb-list-divider";
2377 indexWrapper.appendChild(borderDivider);
2378
2379 var borderDiv = document.createElement("div");
2380 borderDiv.className = "bbb-list-div";
2381 indexWrapper.appendChild(borderDiv);
2382
2383 var borderBarDiv = document.createElement("div");
2384 borderBarDiv.className = "bbb-list-bar";
2385 borderDiv.appendChild(borderBarDiv);
2386
2387 var enableLabel = document.createElement("label");
2388 enableLabel.innerHTML = "Enabled:";
2389 borderBarDiv.appendChild(enableLabel);
2390
2391 var enableBox = document.createElement("input");
2392 enableBox.type = "checkbox";
2393 enableBox.checked = borderItem.is_enabled;
2394 enableBox.addEventListener("click", function(event) {
2395 if (event.button === 0)
2396 borderItem.is_enabled = this.checked;
2397 }, false);
2398 enableLabel.appendChild(enableBox);
2399
2400 var editSpan = document.createElement("span");
2401 editSpan.style.cssFloat = "right";
2402 borderBarDiv.appendChild(editSpan);
2403
2404 var moveButton = document.createElement("a");
2405 moveButton.href = "#";
2406 moveButton.innerHTML = "Move";
2407 moveButton.className = "bbb-list-button";
2408 moveButton.addEventListener("click", function(event) {
2409 if (event.button !== 0)
2410 return;
2411
2412 moveListOption(borderSettings, indexWrapper);
2413 event.preventDefault();
2414 }, false);
2415 moveButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to move this border.");
2416 editSpan.appendChild(moveButton);
2417
2418 var previewButton = document.createElement("a");
2419 previewButton.href = "#";
2420 previewButton.innerHTML = "Preview";
2421 previewButton.className = "bbb-list-button";
2422 previewButton.bbbBorderPreview(borderItem);
2423 editSpan.appendChild(previewButton);
2424
2425 if (!isStatus) {
2426 var deleteButton = document.createElement("a");
2427 deleteButton.href = "#";
2428 deleteButton.innerHTML = "Delete";
2429 deleteButton.className = "bbb-list-button";
2430 deleteButton.addEventListener("click", function(event) {
2431 if (event.button !== 0)
2432 return;
2433
2434 deleteListOption(borderSettings, indexWrapper, "border");
2435 event.preventDefault();
2436 }, false);
2437 editSpan.appendChild(deleteButton);
2438
2439 var newButton = document.createElement("a");
2440 newButton.href = "#";
2441 newButton.innerHTML = "New";
2442 newButton.className = "bbb-list-button";
2443 newButton.addEventListener("click", function(event) {
2444 if (event.button !== 0)
2445 return;
2446
2447 createListOption(borderSettings, indexWrapper, "border");
2448 event.preventDefault();
2449 }, false);
2450 newButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to create a border.");
2451 editSpan.appendChild(newButton);
2452 }
2453
2454 editSpan.appendChild(borderSpacer.cloneNode(false));
2455
2456 var helpButton = document.createElement("a");
2457 helpButton.href = "#";
2458 helpButton.innerHTML = "Help";
2459 helpButton.className = "bbb-list-button";
2460 helpButton.bbbSetTip("<b>Enabled:</b> When checked, the border will be applied. When unchecked, it won't be applied.<tipdesc>Status/Tags:</tipdesc> Describes the posts that the border should be applied to. For custom tag borders, you may specify the rules the post must match for the border to be applied. Please read the \"thumbnail matching rules\" section under the help tab for information about creating rules.<tipdesc>Color:</tipdesc> Set the color of the border. Hex RGB color codes (#000000, #FFFFFF, etc.) are the recommended values.<tipdesc>Style:</tipdesc> Set how the border looks. Please note that double only works with a border width of 3 or higher.<tipdesc>Move:</tipdesc> Move the border to a new position. Higher borders have higher priority. In the event of a post matching more than 4 borders, the first 4 borders get applied and the rest are ignored. If single color borders are enabled, only the first matching border is applied.<tipdesc>Preview:</tipdesc> Display a preview of the border's current settings.<tipdesc>Delete:</tipdesc> Remove the border and its settings.<tipdesc>New:</tipdesc> Create a new border.");
2461 editSpan.appendChild(helpButton);
2462
2463 var borderSettingsDiv = document.createElement("div");
2464 borderSettingsDiv.className = "bbb-list-settings";
2465 borderDiv.appendChild(borderSettingsDiv);
2466
2467 var nameLabel = document.createElement("label");
2468 nameLabel.className = "bbb-list-border-name";
2469 borderSettingsDiv.appendChild(nameLabel);
2470
2471 if (isStatus)
2472 nameLabel.innerHTML = "Status:" + borderItem.tags;
2473 else {
2474 nameLabel.innerHTML = "Tags:";
2475
2476 var nameInput = document.createElement("input");
2477 nameInput.type = "text";
2478 nameInput.value = borderItem.tags + " ";
2479 nameInput.addEventListener("change", function() { borderItem.tags = this.value.bbbTagClean(); }, false);
2480 nameLabel.appendChild(nameInput);
2481 menuAutocomplete(nameInput);
2482
2483 var nameExpand = document.createElement("a");
2484 nameExpand.href = "#";
2485 nameExpand.className = "bbb-edit-link";
2486 nameExpand.innerHTML = "»";
2487 nameExpand.addEventListener("click", function(event) {
2488 if (event.button !== 0)
2489 return;
2490
2491 tagEditWindow(nameInput, borderItem, "tags");
2492 event.preventDefault();
2493 }, false);
2494 nameLabel.appendChild(nameExpand);
2495 }
2496
2497 var colorLabel = document.createElement("label");
2498 colorLabel.innerHTML = "Color:";
2499 colorLabel.className = "bbb-list-border-color";
2500 borderSettingsDiv.appendChild(colorLabel);
2501
2502 var colorInput = document.createElement("input");
2503 colorInput.type = "text";
2504 colorInput.value = borderItem.border_color;
2505 colorInput.addEventListener("change", function() { borderItem.border_color = this.value.bbbSpaceClean(); }, false);
2506 colorLabel.appendChild(colorInput);
2507
2508 var styleLabel = document.createElement("label");
2509 styleLabel.innerHTML = "Style:";
2510 styleLabel.className = "bbb-list-border-style";
2511 borderSettingsDiv.appendChild(styleLabel);
2512
2513 var styleDrop = document.createElement("select");
2514 styleDrop.addEventListener("change", function() { borderItem.border_style = this.value; }, false);
2515 styleLabel.appendChild(styleDrop);
2516
2517 var solidOption = document.createElement("option");
2518 solidOption.innerHTML = "solid";
2519 solidOption.value = "solid";
2520 styleDrop.appendChild(solidOption);
2521
2522 var dashedOption = document.createElement("option");
2523 dashedOption.innerHTML = "dashed";
2524 dashedOption.value = "dashed";
2525 styleDrop.appendChild(dashedOption);
2526
2527 var dottedOption = document.createElement("option");
2528 dottedOption.innerHTML = "dotted";
2529 dottedOption.value = "dotted";
2530 styleDrop.appendChild(dottedOption);
2531
2532 var doubleOption = document.createElement("option");
2533 doubleOption.innerHTML = "double";
2534 doubleOption.value = "double";
2535 styleDrop.appendChild(doubleOption);
2536
2537 var styleOptions = styleDrop.getElementsByTagName("option");
2538
2539 for (var i = 0; i < 4; i++) {
2540 if (styleOptions[i].value === borderItem.border_style) {
2541 styleOptions[i].selected = true;
2542 break;
2543 }
2544 }
2545
2546 return indexWrapper;
2547 }
2548
2549 function createGroupOption(groupSettings, index) {
2550 var groupItem = groupSettings[index];
2551
2552 var groupSpacer = document.createElement("span");
2553 groupSpacer.className = "bbb-list-spacer";
2554
2555 var indexWrapper = document.createElement("div");
2556 indexWrapper.bbbInfo("bbb-index", index);
2557
2558 var groupDivider = document.createElement("div");
2559 groupDivider.className = "bbb-list-divider";
2560 indexWrapper.appendChild(groupDivider);
2561
2562 var groupDiv = document.createElement("div");
2563 groupDiv.className = "bbb-list-div";
2564 indexWrapper.appendChild(groupDiv);
2565
2566 var groupBarDiv = document.createElement("div");
2567 groupBarDiv.className = "bbb-list-bar";
2568 groupDiv.appendChild(groupBarDiv);
2569
2570 var nameLabel = document.createElement("label");
2571 nameLabel.className = "bbb-list-group-name";
2572 nameLabel.innerHTML = "Name:";
2573 groupBarDiv.appendChild(nameLabel);
2574
2575 var nameInput = document.createElement("input");
2576 nameInput.type = "text";
2577 nameInput.value = groupItem.name;
2578 nameInput.addEventListener("input", function() { this.value = this.value.replace(/[\s,]/gi, ""); }, false);
2579 nameInput.addEventListener("change", function() {
2580 var cleanName = this.value.bbbSpaceClean();
2581
2582 if (cleanName === "")
2583 this.value = cleanName = "New_" + timestamp("y-m-d_hh:mm:ss:ms");
2584 else {
2585 for (var i = 0, il = groupSettings.length; i < il; i++) {
2586 if (cleanName === groupSettings[i].name) {
2587 var newName = cleanName + "_" + timestamp("y-m-d_hh:mm:ss:ms");
2588
2589 this.value = cleanName = newName;
2590 bbbDialog("The group name you tried to use is already in use by another group and has been changed to the following:<br>" + newName);
2591 break;
2592 }
2593 }
2594 }
2595
2596 groupItem.name = cleanName;
2597 }, false);
2598 nameLabel.appendChild(nameInput);
2599
2600 var editSpan = document.createElement("span");
2601 editSpan.style.cssFloat = "right";
2602 groupBarDiv.appendChild(editSpan);
2603
2604 var moveButton = document.createElement("a");
2605 moveButton.href = "#";
2606 moveButton.innerHTML = "Move";
2607 moveButton.className = "bbb-list-button";
2608 moveButton.addEventListener("click", function(event) {
2609 if (event.button !== 0)
2610 return;
2611
2612 moveListOption(groupSettings, indexWrapper);
2613 event.preventDefault();
2614 }, false);
2615 moveButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to move this group.");
2616 editSpan.appendChild(moveButton);
2617
2618 var deleteButton = document.createElement("a");
2619 deleteButton.href = "#";
2620 deleteButton.innerHTML = "Delete";
2621 deleteButton.className = "bbb-list-button";
2622 deleteButton.addEventListener("click", function(event) {
2623 if (event.button !== 0)
2624 return;
2625
2626 deleteListOption(groupSettings, indexWrapper, "group");
2627 event.preventDefault();
2628 }, false);
2629 editSpan.appendChild(deleteButton);
2630
2631 var newButton = document.createElement("a");
2632 newButton.href = "#";
2633 newButton.innerHTML = "New";
2634 newButton.className = "bbb-list-button";
2635 newButton.addEventListener("click", function(event) {
2636 if (event.button !== 0)
2637 return;
2638
2639 createListOption(groupSettings, indexWrapper, "group");
2640 event.preventDefault();
2641 }, false);
2642 newButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to create a group.");
2643 editSpan.appendChild(newButton);
2644
2645 editSpan.appendChild(groupSpacer.cloneNode(false));
2646
2647 var helpButton = document.createElement("a");
2648 helpButton.href = "#";
2649 helpButton.innerHTML = "Help";
2650 helpButton.className = "bbb-list-button";
2651 helpButton.bbbSetTip("<b>Name:</b> The name you want to refer to this group of tags by when using the group metatag. Names may not contain spaces or commas. <tipdesc>Tags:</tipdesc> Describes the posts that the group should match. Please read the \"thumbnail matching rules\" section under the help tab for information about creating rules. When used, all tags in a group are treated as if they're grouped/nested together (enclosed in parentheses). <tipdesc>Move:</tipdesc> Move the group to a new position. The order of groups affects the order they display in for tag autocomplete. <tipdesc>Delete:</tipdesc> Remove the group and its settings.<tipdesc>New:</tipdesc> Create a new group. <tiphead>Example</tiphead>If given a group named \"hidden\" that contains \"~loli ~shota ~toddlercon ~status:banned\" for its tags, a search for \"rating:questionable -group:hidden\" would behave like \"rating:questionable -( ~loli ~shota ~toddlercon ~status:banned )\".<tiphead>Tips</tiphead>The \"group\" metatag can be shortened to \"g\". <br><br>Groups used by themselves with the quick search option can provide a saved search functionality.<br><br>A blacklist or border entry (especially a complex one) that you want to work exclusively from other entries can be assigned to a group and excluded from other entries by using \"-group:entryname\". Updating that group will then update all entries linked to it. Similarly, tags you want shared across multiple entries can just use \"group:entryname\".");
2652 editSpan.appendChild(helpButton);
2653
2654 var groupSettingsDiv = document.createElement("div");
2655 groupSettingsDiv.className = "bbb-list-settings";
2656 groupDiv.appendChild(groupSettingsDiv);
2657
2658 var tagsLabel = document.createElement("label");
2659 tagsLabel.className = "bbb-list-group-tags";
2660 tagsLabel.innerHTML = "Tags:";
2661 groupSettingsDiv.appendChild(tagsLabel);
2662
2663 var tagsInput = document.createElement("input");
2664 tagsInput.type = "text";
2665 tagsInput.value = groupItem.tags + " ";
2666 tagsInput.addEventListener("change", function() { groupItem.tags = this.value.bbbTagClean(); }, false);
2667 tagsLabel.appendChild(tagsInput);
2668 menuAutocomplete(tagsInput);
2669
2670 var tagsExpand = document.createElement("a");
2671 tagsExpand.href = "#";
2672 tagsExpand.className = "bbb-edit-link";
2673 tagsExpand.innerHTML = "»";
2674 tagsExpand.addEventListener("click", function(event) {
2675 if (event.button !== 0)
2676 return;
2677
2678 tagEditWindow(tagsInput, groupItem, "tags");
2679 event.preventDefault();
2680 }, false);
2681 tagsLabel.appendChild(tagsExpand);
2682
2683 return indexWrapper;
2684 }
2685
2686 function createTextSection(header, text) {
2687 var sectionFrag = document.createDocumentFragment();
2688
2689 if (header) {
2690 var sectionHeader = document.createElement("h2");
2691 sectionHeader.innerHTML = header;
2692 sectionHeader.className = "bbb-header";
2693 sectionFrag.appendChild(sectionHeader);
2694 }
2695
2696 if (text) {
2697 var desc = document.createElement("div");
2698 desc.innerHTML = text;
2699 desc.className = "bbb-section-text";
2700 sectionFrag.appendChild(desc);
2701 }
2702
2703 return sectionFrag;
2704 }
2705
2706 Element.prototype.bbbTextSection = function(header, text) {
2707 this.appendChild(createTextSection(header, text));
2708 };
2709
2710 function createBackupSection() {
2711 var sectionFrag = document.createDocumentFragment();
2712
2713 var sectionHeader = document.createElement("h2");
2714 sectionHeader.innerHTML = "Backup/Restore Settings";
2715 sectionHeader.className = "bbb-header";
2716 sectionFrag.appendChild(sectionHeader);
2717
2718 var sectionDiv = document.createElement("div");
2719 sectionDiv.className = "bbb-section-options";
2720 sectionFrag.appendChild(sectionDiv);
2721
2722 var backupTextarea = bbb.el.menu.backupTextarea = document.createElement("textarea");
2723 backupTextarea.className = "bbb-backup-area";
2724 sectionDiv.appendChild(backupTextarea);
2725
2726 var buttonDiv = document.createElement("div");
2727 buttonDiv.className = "bbb-section-options";
2728 sectionFrag.appendChild(buttonDiv);
2729
2730 var textBackup = document.createElement("a");
2731 textBackup.innerHTML = "Create Backup Text";
2732 textBackup.href = "#";
2733 textBackup.className = "bbb-button";
2734 textBackup.style.marginRight = "15px";
2735 textBackup.addEventListener("click", function(event) {
2736 if (event.button !== 0)
2737 return;
2738
2739 createBackupText();
2740 event.preventDefault();
2741 }, false);
2742 buttonDiv.appendChild(textBackup);
2743
2744 var pageBackup = document.createElement("a");
2745 pageBackup.innerHTML = "Create Backup Text File";
2746 pageBackup.download = "Better Better Booru v" + bbb.user.bbb_version + " Backup (" + timestamp() + ").txt";
2747 pageBackup.target = "_blank";
2748 pageBackup.href = ('data:,Better Better Booru v' + bbb.user.bbb_version + ' Backup (' + timestamp() + '):%0D%0D' + JSON.stringify(bbb.user)).replace(/#/g, encodeURIComponent("#"));
2749 pageBackup.className = "bbb-button";
2750 pageBackup.style.marginRight = "15px";
2751 buttonDiv.appendChild(pageBackup);
2752
2753 var rightButtons = document.createElement("span");
2754 rightButtons.style.cssFloat = "right";
2755 buttonDiv.appendChild(rightButtons);
2756
2757 var restoreBackup = document.createElement("a");
2758 restoreBackup.innerHTML = "Restore Backup";
2759 restoreBackup.style.marginRight = "15px";
2760 restoreBackup.href = "#";
2761 restoreBackup.className = "bbb-button";
2762 restoreBackup.addEventListener("click", function(event) {
2763 if (event.button !== 0)
2764 return;
2765
2766 restoreBackupText();
2767 event.preventDefault();
2768 }, false);
2769 rightButtons.appendChild(restoreBackup);
2770
2771 var helpButton = document.createElement("a");
2772 helpButton.innerHTML = "Help";
2773 helpButton.href = "#";
2774 helpButton.className = "bbb-button";
2775 helpButton.bbbSetTip("Create copies of your settings that can be used for recovering lost/corrupted settings or transferring settings.<tiphead>Directions</tiphead>There are two options for creating a backup. Creating backup text will provide a plain text format backup in the area provided that can be copied and saved where desired. Creating a backup text file will attempt to download a plain text copy of your settings or open a new tab containing the plain text copy. <br><br>To restore a backup, copy and paste the desired backup into the provided area and click \"restore backup\".");
2776 rightButtons.appendChild(helpButton);
2777
2778 return sectionFrag;
2779 }
2780
2781 Element.prototype.bbbBackupSection = function() {
2782 this.appendChild(createBackupSection());
2783 };
2784
2785 function createEraseSection() {
2786 var sectionFrag = document.createDocumentFragment();
2787
2788 var sectionHeader = document.createElement("h2");
2789 sectionHeader.innerHTML = "Erase Settings";
2790 sectionHeader.className = "bbb-header";
2791 sectionFrag.appendChild(sectionHeader);
2792
2793 var sectionDiv = document.createElement("div");
2794 sectionDiv.innerHTML = "By clicking the button below, you can choose to erase various BBB information from your browser. Please remember to create a backup if you intend to reinstall.";
2795 sectionDiv.className = "bbb-section-options";
2796 sectionFrag.appendChild(sectionDiv);
2797
2798 var buttonDiv = document.createElement("div");
2799 buttonDiv.className = "bbb-section-options";
2800 buttonDiv.style.padding = "5px 0px";
2801 sectionFrag.appendChild(buttonDiv);
2802
2803 var eraseButton = document.createElement("a");
2804 eraseButton.innerHTML = "Erase Options";
2805 eraseButton.href = "#";
2806 eraseButton.className = "bbb-button";
2807 eraseButton.addEventListener("click", function(event) {
2808 if (event.button !== 0)
2809 return;
2810
2811 eraseSettingDialog();
2812 event.preventDefault();
2813 }, false);
2814 buttonDiv.appendChild(eraseButton);
2815
2816 return sectionFrag;
2817 }
2818
2819 Element.prototype.bbbEraseSection = function() {
2820 this.appendChild(createEraseSection());
2821 };
2822
2823 function createBlacklistSection() {
2824 var sectionFrag = document.createDocumentFragment();
2825
2826 var sectionHeader = document.createElement("h2");
2827 sectionHeader.innerHTML = "Blacklist";
2828 sectionHeader.className = "bbb-header";
2829 sectionFrag.appendChild(sectionHeader);
2830
2831 var sectionDiv = document.createElement("div");
2832 sectionDiv.className = "bbb-section-options";
2833 sectionFrag.appendChild(sectionDiv);
2834
2835 var blacklistTextarea = bbb.el.menu.blacklistTextarea = document.createElement("textarea");
2836 blacklistTextarea.className = "bbb-blacklist-area";
2837 blacklistTextarea.value = searchSingleToMulti(bbb.user.script_blacklisted_tags) + " \r\n\r\n";
2838 blacklistTextarea.addEventListener("change", function() { bbb.user.script_blacklisted_tags = searchMultiToSingle(blacklistTextarea.value); }, false);
2839 sectionDiv.appendChild(blacklistTextarea);
2840 menuAutocomplete(blacklistTextarea);
2841
2842 var buttonDiv = document.createElement("div");
2843 buttonDiv.className = "bbb-section-options";
2844 sectionFrag.appendChild(buttonDiv);
2845
2846 var formatButton = document.createElement("a");
2847 formatButton.innerHTML = "Format";
2848 formatButton.href = "#";
2849 formatButton.className = "bbb-button";
2850 formatButton.addEventListener("click", function(event) {
2851 if (event.button !== 0)
2852 return;
2853
2854 var textareaString = searchMultiToSingle(blacklistTextarea.value);
2855
2856 blacklistTextarea.value = searchSingleToMulti(textareaString);
2857 event.preventDefault();
2858 }, false);
2859 buttonDiv.appendChild(formatButton);
2860
2861 var helpButton = document.createElement("a");
2862 helpButton.innerHTML = "Help";
2863 helpButton.href = "#";
2864 helpButton.className = "bbb-button";
2865 helpButton.style.cssFloat = "right";
2866 helpButton.bbbSetTip("Hide posts that match the specified tag(s).<tiphead>Directions</tiphead>Please read the \"thumbnail matching rules\" section under the help tab for information about creating matching rules for posts you wish to blacklist. Blank lines will be ignored and are only used for improved readability.<br><br> All commas outside of tag groups will be converted to new lines and all extra spaces and extra blank lines will be removed the next time the settings are opened. By using the \"format\" button, you can manually perform this action on the blacklist rules. <tiphead>Note</tiphead>When logged in, the account's \"blacklisted tags\" list will override this option. This behavior can be changed with the \"override blacklist\" option under the preferences tab.");
2867 buttonDiv.appendChild(helpButton);
2868
2869 return sectionFrag;
2870 }
2871
2872 Element.prototype.bbbBlacklistSection = function() {
2873 this.appendChild(createBlacklistSection());
2874 };
2875
2876 function createTocSection(page) {
2877 // Generate a Table of Contents based on the page's current section headers.
2878 var sectionFrag = document.createDocumentFragment();
2879 var pageSections = page.getElementsByTagName("h2");
2880
2881 var sectionHeader = document.createElement("h2");
2882 sectionHeader.innerHTML = "Table of Contents";
2883 sectionHeader.className = "bbb-header";
2884 sectionFrag.appendChild(sectionHeader);
2885
2886 var sectionText = document.createElement("div");
2887 sectionText.className = "bbb-section-text";
2888 sectionFrag.appendChild(sectionText);
2889
2890 var tocList = document.createElement("ol");
2891 tocList.className = "bbb-toc";
2892 sectionText.appendChild(tocList);
2893
2894 for (var i = 0, il = pageSections.length; i < il;) {
2895 var listItem = document.createElement("li");
2896 tocList.appendChild(listItem);
2897
2898 var linkItem = document.createElement("a");
2899 linkItem.textContent = pageSections[i].textContent;
2900 linkItem.href = "#" + (++i);
2901 listItem.appendChild(linkItem);
2902 }
2903
2904 tocList.addEventListener("click", function (event) {
2905 var targetValue = event.target.href;
2906
2907 if (event.button !== 0 || !targetValue)
2908 return;
2909
2910 var sectionTop = pageSections[targetValue.split("#")[1]].offsetTop;
2911
2912 bbb.el.menu.scrollDiv.scrollTop = sectionTop;
2913 event.preventDefault();
2914 }, false);
2915
2916 return sectionFrag;
2917 }
2918
2919 Element.prototype.bbbTocSection = function() {
2920 var page = this;
2921 page.insertBefore(createTocSection(page), page.firstElementChild);
2922 };
2923
2924 function newOption(type, def, lbl, expl, optPropObject) {
2925 /*
2926 * Option type notes
2927 * =================
2928 * By specifying a unique type, you can create a specialized menu option.
2929 *
2930 * Checkbox, text, and number do not require any extra properties.
2931 *
2932 * Dropdown requires either txtOptions, numRange, or numList.
2933 * txtOptions = Array containing a list of options and their values separated by a colon. (ex: ["option1:value1", "option2:value2"])
2934 * numRange = Array containing the starting and ending numbers of the number range.
2935 * numList = Array containing a list of the desired numbers.
2936 * If more than one of these is provided, they are added to the list in this order: txtOptions, numList, numRange
2937 */
2938
2939 var option = {
2940 type: type,
2941 def: def, // Default.
2942 label: lbl,
2943 expl: expl // Explanation.
2944 };
2945
2946 if (optPropObject) { // Additional properties provided in the form of an object.
2947 for (var i in optPropObject) {
2948 if (optPropObject.hasOwnProperty(i))
2949 option[i] = optPropObject[i];
2950 }
2951 }
2952
2953 return option;
2954 }
2955
2956 function newSection(type, settingList, header, text) {
2957 /*
2958 * Section type notes
2959 * ==================
2960 * Current section types are general and border.
2961 *
2962 * The setting list for general sections are provided in the form of an array containing the setting names as strings.
2963 * The setting list for border sections is the setting name containing the borders as a string.
2964 */
2965 return {
2966 type: type,
2967 settings: settingList,
2968 header: header,
2969 text: text
2970 };
2971 }
2972
2973 function newBorder(tags, isEnabled, color, style, className) {
2974 return {
2975 tags: tags,
2976 is_enabled: isEnabled,
2977 border_color: color,
2978 border_style: style,
2979 class_name: className
2980 };
2981 }
2982
2983 function borderSet() {
2984 var formatted = [];
2985
2986 for (var i = 0, il = arguments.length; i < il; i++) {
2987 var border = arguments[i];
2988
2989 formatted.push(newBorder(border[0], border[1], border[2], border[3], border[4]));
2990 }
2991
2992 return formatted;
2993 }
2994
2995 function newGroup(name, tags) {
2996 return {
2997 name: name,
2998 tags: tags
2999 };
3000 }
3001
3002 function groupSet() {
3003 var formatted = [];
3004
3005 for (var i = 0, il = arguments.length; i < il; i++) {
3006 var group = arguments[i];
3007
3008 formatted.push(newGroup(group[0], group[1]));
3009 }
3010
3011 return formatted;
3012 }
3013
3014 function resetListElements(section) {
3015 // Reset the list of items after moving or creating a new item.
3016 var optionElements = section.children;
3017
3018 for (var i = 0, il = optionElements.length; i < il; i++) {
3019 var optionElement = optionElements[i];
3020
3021 optionElement.bbbRemoveClass("bbb-no-highlight");
3022 optionElement.bbbInfo("bbb-index", i);
3023 }
3024 }
3025
3026 function deleteListOption(listSettings, optionElement, type) {
3027 // Remove an item and if it's the last one, create a blank one.
3028 var section = optionElement.parentNode;
3029 var index = Number(optionElement.bbbInfo("bbb-index"));
3030
3031 section.removeChild(optionElement);
3032 listSettings.splice(index,1);
3033
3034 if (!listSettings[0]) {
3035 // If no borders are left, add a new blank border.
3036 var newListItem = (type === "border" ? newBorder("", false, "#000000", "solid") : newGroup("New_" + timestamp("y-m-d_hh:mm:ss:ms"), "", false));
3037 listSettings.push(newListItem);
3038
3039 var newOptionElement = (type === "border" ? createBorderOption(listSettings, 0) : createGroupOption(listSettings, 0));
3040 section.insertBefore(newOptionElement, section.firstElementChild);
3041 }
3042
3043 resetListElements(section);
3044 }
3045
3046 function moveListOption(listSettings, optionElement) {
3047 // Prepare to move an item and wait for the user to click where it'll go.
3048 var section = optionElement.parentNode;
3049 var index = Number(optionElement.bbbInfo("bbb-index"));
3050
3051 optionElement.bbbAddClass("bbb-no-highlight");
3052 optionElement.nextSibling.bbbAddClass("bbb-no-highlight");
3053 section.bbbAddClass("bbb-insert-highlight");
3054 bbb.el.menu.window.addEventListener("click", function insertListOption(event) {
3055 var target = event.target;
3056
3057 if (target.className === "bbb-list-divider" && event.button === 0) {
3058 var newIndex = Number(target.parentNode.bbbInfo("bbb-index"));
3059
3060 if (newIndex !== index) {
3061 var listItem = listSettings.splice(index, 1)[0];
3062
3063 if (newIndex < index)
3064 listSettings.splice(newIndex, 0, listItem);
3065 else if (newIndex > index)
3066 listSettings.splice(newIndex - 1, 0, listItem);
3067
3068 section.insertBefore(optionElement, section.children[newIndex]);
3069 }
3070 }
3071
3072 resetListElements(section);
3073 section.bbbRemoveClass("bbb-insert-highlight");
3074 bbb.el.menu.window.removeEventListener("click", insertListOption, true);
3075 }, true);
3076 }
3077
3078 function createListOption(listSettings, optionElement, type) {
3079 // Prepare to create an item and wait for the user to click where it'll go.
3080 var section = optionElement.parentNode;
3081
3082 section.bbbAddClass("bbb-insert-highlight");
3083 bbb.el.menu.window.addEventListener("click", function insertListOption(event) {
3084 var target = event.target;
3085
3086 if (target.className === "bbb-list-divider" && event.button === 0) {
3087 var newIndex = Number(target.parentNode.bbbInfo("bbb-index"));
3088 var newListItem = (type === "border" ? newBorder("", false, "#000000", "solid") : newGroup("New_" + timestamp("y-m-d_hh:mm:ss:ms"), "", false));
3089
3090 listSettings.splice(newIndex, 0, newListItem);
3091
3092 var newOptionElement = (type === "border" ? createBorderOption(listSettings, newIndex) : createGroupOption(listSettings, newIndex));
3093
3094 section.insertBefore(newOptionElement, section.children[newIndex]);
3095 }
3096
3097 resetListElements(section);
3098 section.bbbRemoveClass("bbb-insert-highlight");
3099 bbb.el.menu.window.removeEventListener("click", insertListOption, true);
3100 }, true);
3101 }
3102
3103 function showTip(event, content, styleString) {
3104 var x = event.clientX;
3105 var y = event.clientY;
3106 var tip = bbb.el.menu.tip;
3107
3108 if (styleString)
3109 tip.setAttribute("style", styleString);
3110
3111 formatTip(event, tip, content, x, y);
3112 }
3113
3114 function hideTip() {
3115 bbb.el.menu.tip.removeAttribute("style");
3116 }
3117
3118 Element.prototype.bbbBorderPreview = function(borderItem) {
3119 this.addEventListener("click", function(event) {
3120 if (event.button !== 0)
3121 return;
3122
3123 showTip(event, "<img src=\"http://danbooru.donmai.us/data/preview/d34e4cf0a437a5d65f8e82b7bcd02606.jpg\" alt=\"IMAGE\" style=\"width: 105px; height: 150px; border-color: " + borderItem.border_color + "; border-style: " + borderItem.border_style + "; border-width: " + bbb.user.border_width + "px; padding:" + bbb.user.border_spacing + "px; line-height: 150px; text-align: center; vertical-align: middle;\">", "background-color: #FFFFFF;");
3124 event.preventDefault();
3125 }, false);
3126 this.addEventListener("mouseout", hideTip, false);
3127 };
3128
3129 Element.prototype.bbbSetTip = function(text) {
3130 var tip = bbb.el.menu.tip;
3131
3132 this.addEventListener("click", function(event) {
3133 if (event.button !== 0)
3134 return;
3135
3136 showTip(event, text, false);
3137 event.preventDefault();
3138 }, false);
3139 this.addEventListener("mouseout", function() { bbb.timers.hideTip = window.setTimeout(hideTip, 100); }, false);
3140 tip.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.hideTip); }, false);
3141 tip.addEventListener("mouseleave", hideTip, false);
3142 };
3143
3144 function changeTab(tab) {
3145 var activeTab = document.getElementsByClassName("bbb-active-tab")[0];
3146
3147 if (tab === activeTab)
3148 return;
3149
3150 activeTab.bbbRemoveClass("bbb-active-tab");
3151 bbb.el.menu[activeTab.name + "Page"].style.display = "none";
3152 bbb.el.menu.scrollDiv.scrollTop = 0;
3153 tab.bbbAddClass("bbb-active-tab");
3154 bbb.el.menu[tab.name + "Page"].style.display = "block";
3155 }
3156
3157 function tagEditWindow(input, object, prop) {
3158 var tagEditBlocker = document.createDocumentFragment();
3159
3160 var tagEditHeader = document.createElement("h2");
3161 tagEditHeader.innerHTML = "Tag Editor";
3162 tagEditHeader.className = "bbb-header";
3163 tagEditBlocker.appendChild(tagEditHeader);
3164
3165 var tagEditArea = bbb.el.menu.tagEditArea = document.createElement("textarea");
3166 tagEditArea.value = searchSingleToMulti(input.value) + " \r\n\r\n";
3167 tagEditArea.className = "bbb-edit-area";
3168 tagEditBlocker.appendChild(tagEditArea);
3169
3170 var tagEditOk = function() {
3171 var tags = searchMultiToSingle(tagEditArea.value);
3172
3173 input.value = tags + " ";
3174 input.focus();
3175 input.setSelectionRange(0, 0);
3176 object[prop] = tags;
3177 };
3178
3179 bbbDialog(tagEditBlocker, {ok: tagEditOk, cancel: true});
3180 tagEditArea.focus();
3181 tagEditArea.setSelectionRange(0, 0);
3182
3183 menuAutocomplete(tagEditArea);
3184 }
3185
3186 function adjustMenuHeight() {
3187 var menu = bbb.el.menu.window;
3188 var scrollDiv = bbb.el.menu.scrollDiv;
3189 var viewHeight = document.documentElement.clientHeight;
3190 var scrollDivDiff = menu.offsetHeight - scrollDiv.clientHeight;
3191
3192 scrollDiv.style.maxHeight = viewHeight - scrollDiv.bbbGetPadding().height - scrollDivDiff - 50 + "px"; // Subtract 50 for margins (25 each).
3193 bbb.timers.adjustMenu = 0;
3194 }
3195
3196 function adjustMenuTimer() {
3197 if (!bbb.timers.adjustMenu && bbb.el.menu.window)
3198 bbb.timers.adjustMenu = window.setTimeout(adjustMenuHeight, 50);
3199 }
3200
3201 function removeMenu() {
3202 // Destroy the menu so that it gets rebuilt.
3203 var menu = bbb.el.menu.window;
3204
3205 if (!menu)
3206 return;
3207
3208 menu.parentNode.removeChild(menu);
3209 bbb.el.menu = {};
3210 }
3211
3212 function loadSettings() {
3213 // Load stored settings.
3214 var settings = loadData("bbb_settings");
3215
3216 if (settings === null) {
3217 // Settings not found in the expected place.
3218 var domain = location.protocol + "//" + location.hostname;
3219 var localSettings = localStorage.getItem("bbb_settings");
3220
3221 if (localSettings !== null) {
3222 // Load up the localStorage settings if they exist.
3223 bbb.user = parseJson(localSettings, jsonSettingsErrorHandler);
3224 checkUser(bbb.user, bbb.options);
3225
3226 if (bbb.user.bbb_version !== bbb.options.bbb_version)
3227 convertSettings("load");
3228
3229 if (!bbb.flags.new_storage_notice) {
3230 // Alert the user when there are no GM storage settings but there are localStorage settings.
3231 bbb.flags.new_storage_notice = true;
3232
3233 var newStorageNotice = function() {
3234 bbbDialog('As of version 7.3, BBB has changed the way it saves information. You currently don\'t have any settings saved with the new method, but do have settings saved with the old method. Before clicking anything, please read the following:<ul><li>Please check the BBB settings menu and confirm that everything is correct. If it is, click "save & close" to transfer your settings over. If your settings are not correct, you may restore from a backup and then save.</li><li>Alternatively, if ' + domain + ' (' + (/^https/.test(domain) ? '' : 'not ') + 'secure/https) is not the Danbooru domain you usually use, you should change to your usual domain and retry checking your settings there.</li><li>Finally, if neither of those steps work, you should <a href="https://sleazyfork.org/scripts/3575-better-better-booru/code/better_better_booru.user.js?version=123120&d=.user.js">revert to version 7.2.5</a>, refresh/reload Danbooru, create a backup, and then update back to version 7.3.</li></ul>');
3235 openMenu();
3236
3237 document.removeEventListener("mousemove", newStorageNotice, false);
3238 };
3239
3240 document.addEventListener("mousemove", newStorageNotice, false);
3241 }
3242 }
3243 else {
3244 // Load defaults.
3245 if (!getCookie().bbb_no_settings && !bbb.flags.local_storage_full) {
3246 // Alert the user so that new users know what to do and other users are know their usual settings aren't in effect.
3247 var noSettingsNotice = function() {
3248 if (!getCookie().bbb_no_settings) { // Trigger the notice if it hasn't been displayed in another tab/window.
3249 bbbNotice("No settings could be detected for " + domain + ". Please take a moment to set/restore your options by using the \"BBB settings\" link in the Danbooru navigation bar.", 15);
3250 createCookie("bbb_no_settings", 1);
3251 }
3252
3253 document.removeEventListener("mousemove", noSettingsNotice, false);
3254 };
3255
3256 document.addEventListener("mousemove", noSettingsNotice, false);
3257 }
3258
3259 loadDefaults();
3260 }
3261 }
3262 else if (typeof(settings) === "string") {
3263 bbb.user = parseJson(settings, jsonSettingsErrorHandler);
3264 checkUser(bbb.user, bbb.options);
3265
3266 if (bbb.user.bbb_version !== bbb.options.bbb_version) {
3267 convertSettings("load");
3268 saveSettings();
3269 }
3270 }
3271 }
3272
3273 function loadDefaults() {
3274 // Load the default settings.
3275 bbb.user = defaultSettings();
3276 }
3277
3278 function defaultSettings() {
3279 // Create a copy of the default settings.
3280 var defaults = {};
3281
3282 for (var i in bbb.options) {
3283 if (bbb.options.hasOwnProperty(i)) {
3284 if (typeof(bbb.options[i].def) !== "undefined")
3285 defaults[i] = bbb.options[i].def;
3286 else
3287 defaults[i] = bbb.options[i];
3288 }
3289 }
3290
3291 return defaults;
3292 }
3293
3294 function checkUser(user, options) {
3295 // Verify the user has all the base settings and add them with their default values if they don't.
3296 for (var i in options) {
3297 if (options.hasOwnProperty(i)) {
3298 if (typeof(user[i]) === "undefined") {
3299 if (typeof(options[i].def) !== "undefined")
3300 user[i] = options[i].def;
3301 else
3302 user[i] = options[i];
3303 }
3304 else if (typeof(user[i]) === "object" && !(user[i] instanceof Array))
3305 checkUser(user[i], options[i]);
3306 }
3307 }
3308 }
3309
3310 function saveSettings() {
3311 // Save the user settings to localStorage after making any necessary checks/adjustments.
3312 if (bbb.settings.changed.track_new && !bbb.user.track_new && bbb.user.track_new_data.viewed) // Reset new post tracking if it has been disabled.
3313 bbb.user.track_new_data = bbb.options.track_new_data.def;
3314
3315 if (bbb.settings.changed.thumbnail_count) // Update the link limit values if the user has changed the value.
3316 fixLimit(bbb.user.thumbnail_count);
3317
3318 if (bbb.settings.changed.blacklist_highlight_color && bbb.user.blacklist_highlight_color === "") // Use the default highlight color if the field is left blank.
3319 bbb.user.blacklist_highlight_color = "#CCCCCC";
3320
3321 bbb.settings.changed = {};
3322
3323 // Remove the no settings cookie so that it can display again and signal an account settings check for brand new settings.
3324 if (getCookie().bbb_no_settings) {
3325 createCookie("bbb_no_settings", 0, -1);
3326 createCookie("bbb_acc_check", 1);
3327 }
3328
3329 saveData("bbb_settings", JSON.stringify(bbb.user));
3330 }
3331
3332 function updateSettings() {
3333 // Change & save the settings without the panel. Accepts a comma delimited list of alternating settings and values: setting1, value1, setting2, value2
3334 loadSettings();
3335
3336 for (var i = 0, il = arguments.length; i < il; i += 2) {
3337 var setting = arguments[i].split(".");
3338 var value = arguments[i + 1];
3339 var settingPath = bbb.user;
3340
3341 for (var j = 0, jl = setting.length - 1; j < jl; j++)
3342 settingPath = settingPath[setting[j]];
3343
3344 settingPath[setting[j]] = value;
3345 bbb.settings.changed[setting[j]] = true;
3346 }
3347
3348 saveSettings();
3349 }
3350
3351 function convertSettings(reason) {
3352 // If the user settings are from an old version, attempt to convert some settings and update the version number. Settings will start conversion at the appropriate case and be allowed to run through every case after it until the end.
3353 var userVer = bbb.user.bbb_version;
3354 var scriptVer = bbb.options.bbb_version;
3355
3356 if (isOldVersion(userVer)) {
3357 switch (userVer) {
3358 case "6.0.2":
3359 if (bbb.user.tag_scrollbars === "false")
3360 bbb.user.tag_scrollbars = 0;
3361
3362 case "6.1":
3363 case "6.2":
3364 case "6.2.1":
3365 case "6.2.2":
3366 // Convert the old hide_original_notice setting to the new show_resized_notice setting that replaces it.
3367 if (bbb.user.hide_original_notice)
3368 bbb.user.show_resized_notice = "sample";
3369
3370 // Set the new show_banned setting to true if show_deleted is true.
3371 if (bbb.user.show_deleted)
3372 bbb.user.show_banned = true;
3373
3374 // Add a custom border for banned posts to match the other hidden post borders.
3375 if (!/(?:^|\s)status:banned(?:$|\s)/i.test(JSON.stringify(bbb.user.tag_borders)))
3376 bbb.user.tag_borders.push(newBorder("status:banned", false, "#000000", "solid"));
3377
3378 // Warn about uninstalling old version from Userscripts.org
3379 if (reason !== "backup")
3380 bbbNotice("You have just been updated from a version of this script that was hosted on Userscripts.org. Before continuing any further, please open your userscript manager and remove any versions of this script older than version 6.3 that may be there.", 0);
3381
3382 case "6.3":
3383 case "6.3.1":
3384 case "6.3.2":
3385 case "6.4":
3386 case "6.5":
3387 case "6.5.1":
3388 case "6.5.2":
3389 case "6.5.3":
3390 case "6.5.4":
3391 // Copy over settings to their new names.
3392 if (bbb.user.image_drag_scroll)
3393 bbb.user.post_drag_scroll = bbb.user.image_drag_scroll;
3394
3395 if (bbb.user.image_resize)
3396 bbb.user.post_resize = bbb.user.image_resize;
3397
3398 if (bbb.user.image_resize_mode)
3399 bbb.user.post_resize_mode = bbb.user.image_resize_mode;
3400
3401 if (bbb.user.tag_scrollbars)
3402 bbb.user.post_tag_scrollbars = bbb.user.tag_scrollbars;
3403
3404 // Convert old settings.
3405 if (bbb.user.autoscroll_image)
3406 bbb.user.autoscroll_post = "post";
3407
3408 if (bbb.user.search_add)
3409 bbb.user.search_add = "link";
3410
3411 if (bbb.user.override_account) {
3412 bbb.user.override_blacklist = "always";
3413 bbb.user.override_resize = true;
3414 bbb.user.override_sample = true;
3415 }
3416
3417 case "7.0":
3418 case "7.1":
3419 case "7.2":
3420 case "7.2.1":
3421 case "7.2.2":
3422 case "7.2.3":
3423 case "7.2.4":
3424 case "7.2.5":
3425 case "7.3":
3426 case "7.4":
3427 case "7.4.1":
3428 if (reason !== "backup")
3429 bbbNotice("As of version 7.4.1, the options related to hidden/censored posts have been changed to placeholder options due to Danbooru finally fixing their loopholes.", 0);
3430
3431 deleteData("bbb_thumb_cache");
3432 case "8.0":
3433 case "8.0.1":
3434 case "8.0.2":
3435 case "8.1":
3436 case "8.2":
3437 case "8.2.1":
3438 case "8.2.2":
3439 break;
3440 }
3441
3442 cleanUser();
3443 bbb.user.bbb_version = scriptVer;
3444 }
3445 else if (userVer !== scriptVer) // Revert the version number for downgrades so that conversion can properly work on the settings again for a future upgrade.
3446 bbb.user.bbb_version = scriptVer;
3447 }
3448
3449 function cleanUser() {
3450 // Verify the user doesn't have any settings that aren't in the base settings and delete them if they do.
3451 var user = bbb.user;
3452
3453 for (var i in user) {
3454 if (user.hasOwnProperty(i) && typeof(bbb.options[i]) === "undefined")
3455 delete user[i];
3456 }
3457 }
3458
3459 function eraseSettings() {
3460 // Try to erase everything.
3461 var gmList = listData();
3462 var cookies = getCookie();
3463 var i, il, keyName; // Loop variables.
3464
3465 for (i = 0, il = gmList.length; i < il; i++)
3466 deleteData(gmList[i]);
3467
3468 for (i = localStorage.length - 1; i >= 0; i--) {
3469 keyName = localStorage.key(i);
3470
3471 if (keyName.indexOf("bbb_") === 0)
3472 localStorage.removeItem(keyName);
3473 }
3474
3475 for (i = sessionStorage.length - 1; i >= 0; i--) {
3476 keyName = sessionStorage.key(i);
3477
3478 if (keyName.indexOf("bbb_") === 0)
3479 sessionStorage.removeItem(keyName);
3480 }
3481
3482 for (i in cookies) {
3483 if (cookies.hasOwnProperty(i) && i.indexOf("bbb_") === 0)
3484 createCookie(i, 0, -1);
3485 }
3486 }
3487
3488 function createBackupText() {
3489 // Create a plain text version of the settings.
3490 var textarea = bbb.el.menu.backupTextarea;
3491 textarea.value = "Better Better Booru v" + bbb.user.bbb_version + " Backup (" + timestamp() + "):\r\n\r\n" + JSON.stringify(bbb.user) + "\r\n";
3492 textarea.focus();
3493 textarea.setSelectionRange(0,0);
3494 }
3495
3496 function restoreBackupText() {
3497 // Load the backup text provided into the script.
3498 var textarea = bbb.el.menu.backupTextarea;
3499 var backupString = textarea.value.replace(/\r?\n/g, "").match(/\{.+\}/);
3500
3501 if (backupString) {
3502 try {
3503 bbb.user = parseJson(backupString); // This is where we expect an error.
3504 checkUser(bbb.user, bbb.options);
3505 convertSettings("backup");
3506 reloadMenu();
3507 bbbDialog("Backup settings loaded successfully. After reviewing the settings to ensure they are correct, please click \"save & close\" to finalize the restore.");
3508 }
3509 catch (error) {
3510 if (error instanceof SyntaxError)
3511 bbbDialog("The backup does not appear to be formatted correctly. Please make sure everything was pasted correctly/completely and that only one backup is provided.");
3512 else
3513 bbbDialog("Unexpected error: " + error.message);
3514 }
3515 }
3516 else
3517 bbbDialog("A backup could not be detected in the text provided. Please make sure everything was pasted correctly/completely.");
3518 }
3519
3520 /* Post functions */
3521 function swapImageInit() {
3522 // Create the custom elements for swapping between the sample and original images and set them up.
3523 createSwapElements();
3524
3525 if (image_swap_mode === "load")
3526 swapImageLoad();
3527 else if (image_swap_mode === "view")
3528 swapImageView();
3529 }
3530
3531 function createSwapElements() {
3532 // Create the elements for swapping between the original and sample image.
3533 var postInfo = bbb.post.info;
3534
3535 if (!postInfo.has_large)
3536 return;
3537
3538 // Remove the original notice (it's not always there) and replace it with our own.
3539 var img = document.getElementById("image");
3540 var imgContainer = document.getElementById("image-container");
3541 var resizeNotice = document.getElementById("image-resize-notice");
3542
3543 if (resizeNotice)
3544 resizeNotice.parentNode.removeChild(resizeNotice);
3545
3546 var bbbResizeNotice = bbb.el.resizeNotice = document.createElement("div");
3547 bbbResizeNotice.id = "image-resize-notice";
3548 bbbResizeNotice.className = "ui-corner-all ui-state-highlight notice notice-resized";
3549 bbbResizeNotice.style.position = "relative";
3550 bbbResizeNotice.style.display = "none";
3551 bbbResizeNotice.innerHTML = '<span id="bbb-resize-status"></span> (<a href="" id="bbb-resize-link"></a>)<span style="display: block;" class="close-button ui-icon ui-icon-closethick" id="close-resize-notice"></span>';
3552
3553 var resizeStatus = bbb.el.resizeStatus = getId("bbb-resize-status", bbbResizeNotice);
3554 var resizeLink = bbb.el.resizeLink = getId("bbb-resize-link", bbbResizeNotice);
3555 var closeResizeNotice = bbb.el.closeResizeNotice = getId("close-resize-notice", bbbResizeNotice);
3556
3557 closeResizeNotice.addEventListener("click", function(event) {
3558 if (event.button !== 0)
3559 return;
3560
3561 var showResNot = bbb.user.show_resized_notice;
3562
3563 bbbResizeNotice.style.display = "none";
3564
3565 if (img.src.indexOf("/sample/") < 0) { // Original image.
3566 if (showResNot === "original")
3567 showResNot = "none";
3568 else if (showResNot === "all")
3569 showResNot = "sample";
3570
3571 bbbNotice("Settings updated. The resized notice will now be hidden when viewing original images. You may change this setting under \"notices\" in the settings panel.", 10);
3572 }
3573 else { // Sample image.
3574 if (showResNot === "sample")
3575 showResNot = "none";
3576 else if (showResNot === "all")
3577 showResNot = "original";
3578
3579 bbbNotice("Settings updated. The resized notice will now be hidden when viewing sample images. You may change this setting under \"notices\" in the settings panel.", 10);
3580 }
3581
3582 updateSettings("show_resized_notice", showResNot);
3583 }, false);
3584
3585 // Create a swap image link in the sidebar options section.
3586 var firstOption = document.querySelector("#post-options ul li");
3587
3588 if (firstOption) {
3589 var swapListItem = document.createElement("li");
3590
3591 var swapLink = bbb.el.swapLink = document.createElement("a");
3592 swapListItem.appendChild(swapLink);
3593
3594 swapLink.addEventListener("click", function(event) {
3595 if (event.button !== 0)
3596 return;
3597
3598 swapPost();
3599 event.preventDefault();
3600 }, false);
3601
3602 // Prepare the element text, etc.
3603 swapImageUpdate((load_sample_first ? "sample" : "original"));
3604
3605 // Add the elements to the document.
3606 imgContainer.parentNode.insertBefore(bbbResizeNotice, imgContainer);
3607
3608 firstOption.parentNode.insertBefore(swapListItem, firstOption);
3609 }
3610 }
3611
3612 function swapImageLoad() {
3613 // Set up the post to load the content before displaying it.
3614 var postInfo = bbb.post.info;
3615
3616 if (!postInfo.has_large)
3617 return;
3618
3619 var img = document.getElementById("image");
3620 var bbbLoader = bbb.el.bbbLoader;
3621 var resizeStatus = bbb.el.resizeStatus;
3622 var resizeLink = bbb.el.resizeLink;
3623 var swapLink = bbb.el.swapLink;
3624
3625 resizeLink.addEventListener("click", function(event) {
3626 if (event.button !== 0)
3627 return;
3628
3629 swapPost();
3630 event.preventDefault();
3631 }, false);
3632 bbbLoader.addEventListener("load", function() { // Change the image to the successfully loaded sample/original image.
3633 if (!bbb.post.swapped)
3634 bbb.post.swapped = true;
3635
3636 if (bbbLoader.src !== "about:blank") {
3637 img.src = bbbLoader.src;
3638 bbbLoader.src = "about:blank";
3639 }
3640 }, false);
3641 bbbLoader.addEventListener("error", function(event) { // State the image has failed loading and provide a retry link.
3642 if (bbbLoader.src !== "about:blank") {
3643 var currentImg = (bbbLoader.src.indexOf("/sample/") < 0 ? "Original" : "Sample");
3644
3645 resizeStatus.innerHTML = currentImg + " image loading failed!";
3646 resizeLink.innerHTML = "retry";
3647 swapLink.innerHTML = "View " + currentImg.toLowerCase();
3648 bbbLoader.src = "about:blank";
3649 }
3650
3651 event.preventDefault();
3652 }, false);
3653 img.addEventListener("load", function() { // Update the swap image elements.
3654 if (bbbLoader.src === "about:blank") {
3655 if (img.src.indexOf("/sample/") < 0) // Original image loaded.
3656 swapImageUpdate("original");
3657 else // Sample image loaded.
3658 swapImageUpdate("sample");
3659 }
3660
3661 if (bbb.post.swapped)
3662 resizePost("swap");
3663 }, false);
3664 }
3665
3666 function swapImageView() {
3667 // Set up the post to display the content as it loads.
3668 var postInfo = bbb.post.info;
3669
3670 if (!postInfo.has_large)
3671 return;
3672
3673 var img = document.getElementById("image");
3674 var resizeStatus = bbb.el.resizeStatus;
3675 var resizeLink = bbb.el.resizeLink;
3676 var swapLink = bbb.el.swapLink;
3677
3678 resizeLink.addEventListener("click", function(event) {
3679 if (event.button !== 0)
3680 return;
3681
3682 swapPost();
3683 event.preventDefault();
3684 }, false);
3685 img.addEventListener("error", function(event) { // State the image has failed loading and provide a link to the other image.
3686 if (img.src !== "about:blank") {
3687 var currentImg = (img.src.indexOf("/sample/") < 0 ? "Original" : "Sample");
3688 var otherImg = (currentImg === "Original" ? "sample" : "original");
3689
3690 resizeStatus.innerHTML = currentImg + " image loading failed!";
3691 resizeLink.innerHTML = "view " + otherImg;
3692 swapLink.innerHTML = "View " + otherImg;
3693 }
3694
3695 event.preventDefault();
3696 }, false);
3697 }
3698
3699 function noteToggleInit() {
3700 // Override Danbooru's image click handler for toggling notes with a custom one.
3701 var image = document.getElementById("image");
3702
3703 if (!image)
3704 return;
3705
3706 image.bbbOverrideClick(function() {
3707 if (!Danbooru.Note.TranslationMode.active && !bbb.drag_scroll.moved)
3708 Danbooru.Note.Box.toggle_all();
3709 });
3710 }
3711
3712 function noteToggleLinkInit() {
3713 // Make a "toggle notes" link in the sidebar options or prepare an existing link.
3714 var toggleLink = document.getElementById("bbb-note-toggle");
3715
3716 if (!toggleLink) {
3717 var before = document.getElementById((isLoggedIn() ? "add-notes-list" : "random-post"));
3718
3719 if (before) {
3720 var listNoteToggle = document.createElement("li");
3721 listNoteToggle.innerHTML = '<a href="#" id="bbb-note-toggle">Toggle notes</a>';
3722 before.parentNode.insertBefore(listNoteToggle, before);
3723 toggleLink = document.getElementById("bbb-note-toggle");
3724 }
3725 }
3726
3727 if (toggleLink) {
3728 document.getElementById("bbb-note-toggle").addEventListener("click", function(event) {
3729 if (event.button !== 0)
3730 return;
3731
3732 Danbooru.Note.Box.toggle_all();
3733 event.preventDefault();
3734 }, false);
3735 }
3736 }
3737
3738 function translationModeInit() {
3739 // Set up translation mode.
3740 var postInfo = bbb.post.info;
3741 var postContent = getPostContent();
3742 var postEl = postContent.el;
3743 var postTag = (postEl ? postEl.tagName : undefined);
3744 var translateLink = document.getElementById("translate");
3745 var toggleFunction; // If/else variable.
3746
3747 if (postInfo.file_ext !== "webm" && postInfo.file_ext !== "mp4" && postInfo.file_ext !== "swf") { // Don't allow translation functions on videos or flash.
3748 if (postTag !== "VIDEO") { // Make translation mode work on non-video content.
3749 // Set up/override the translate link and hotkey if notes aren't locked.
3750 if (!document.getElementById("note-locked-notice") && translateLink) {
3751 translateLink.bbbOverrideClick(Danbooru.Note.TranslationMode.toggle);
3752 createHotkey("78", Danbooru.Note.TranslationMode.toggle);
3753 }
3754 }
3755 else { // Allow note viewing on ugoira webm video samples, but don't allow editing.
3756 toggleFunction = function(event) {
3757 bbbNotice('Note editing is not allowed while using the ugoira video sample. Please use the <a href="' + updateURLQuery(location.href, {original: "1"}) + '">original</a> ugoira version for note editing.', -1);
3758 event.preventDefault();
3759 };
3760
3761 Danbooru.Note.TranslationMode.start = toggleFunction;
3762 Danbooru.Note.Edit.show = toggleFunction;
3763
3764 if (translateLink)
3765 translateLink.bbbOverrideClick(toggleFunction);
3766
3767 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3768 }
3769 }
3770 else { // Provide a warning for unsupported content.
3771 toggleFunction = function(event) {
3772 bbbNotice('Note editing is not allowed on flash/video content.', -1);
3773 event.preventDefault();
3774 };
3775
3776 Danbooru.Note.TranslationMode.start = toggleFunction;
3777 Danbooru.Note.Edit.show = toggleFunction;
3778
3779 if (translateLink)
3780 translateLink.bbbOverrideClick(toggleFunction);
3781
3782 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3783 }
3784 }
3785
3786 function disableEmbeddedNotes() {
3787 // Disable embedded notes for viewing and re-enable them for note editing.
3788 var useEmbedded = (getMeta("post-has-embedded-notes") === "true");
3789 var noteContainer = document.getElementById("note-container");
3790 var notesSection = document.getElementById("notes");
3791 var notes = (notesSection ? notesSection.getElementsByTagName("article") : undefined);
3792
3793 if (!disable_embedded_notes || !useEmbedded || !notes[0])
3794 return;
3795
3796 Danbooru.Note.embed = false;
3797
3798 var postInfo = bbb.post.info;
3799 var postContent = getPostContent();
3800 var postEl = postContent.el;
3801 var postTag = (postEl ? postEl.tagName : undefined);
3802 var notLocked = !document.getElementById("note-locked-notice");
3803
3804 // Stop here for content that doesn't allow note editing.
3805 if (postInfo.file_ext === "webm" || postInfo.file_ext === "mp4" || postInfo.file_ext === "swf" || postTag === "VIDEO")
3806 return;
3807
3808 // Save the original note functions.
3809 var origEditFunction = Danbooru.Note.Edit.show;
3810
3811 // Create override functions.
3812 var toggleFunction = function(event) {
3813 var translateLink = document.getElementById("translate");
3814
3815 if (event.type === "click" && (event.target !== translateLink || event.button !== 0))
3816 return;
3817
3818 resetFunction();
3819
3820 if (notLocked)
3821 Danbooru.Note.TranslationMode.toggle(event);
3822
3823 event.preventDefault();
3824 event.stopPropagation();
3825 };
3826
3827 var editFunction = function(editTarget) { // This function is actually assigned under an anonymous function in Danbooru. The first argument is an element from the div.
3828 resetFunction();
3829 origEditFunction(editTarget);
3830 };
3831
3832 var resetFunction = function() {
3833 // Remove all overrides/overwrites.
3834 Danbooru.Note.Edit.show = origEditFunction;
3835 document.removeEventListener("click", toggleFunction, true);
3836
3837 if (notLocked)
3838 createHotkey("78", Danbooru.Note.TranslationMode.toggle);
3839 else
3840 removeHotkey("78");
3841
3842 // Reset notes with embedded notes enabled.
3843 Danbooru.Note.embed = true;
3844 noteContainer.innerHTML = "";
3845 Danbooru.Note.load_all("bbb");
3846 };
3847
3848 document.addEventListener("click", toggleFunction, true); // Override all other click events for the translate link.
3849 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3850 Danbooru.Note.Edit.show = editFunction; // Overwrite the note edit function.
3851 }
3852
3853 function alternateImageSwap() {
3854 // Override Danbooru's image click handler for toggling notes with a custom one that swaps the image.
3855 var postInfo = bbb.post.info;
3856 var image = document.getElementById("image");
3857
3858 if (postInfo.has_large && image) {
3859 image.bbbOverrideClick(function() {
3860 if (!Danbooru.Note.TranslationMode.active && !bbb.drag_scroll.moved)
3861 swapPost();
3862 });
3863 }
3864
3865 // Set up the "toggle notes" link since the image won't be used for toggling.
3866 noteToggleLinkInit();
3867 }
3868
3869 function fixOptionsSection() {
3870 // Fix the sidebar options section for logged out users.
3871 if (isLoggedIn())
3872 return;
3873
3874 var postInfo = bbb.post.info;
3875 var optionsSection = document.getElementById("post-options");
3876
3877 optionsSection.innerHTML = '<h1>Options</h1><ul><li><a href="#" id="image-resize-to-window-link">Resize to window</a></li><li>Download</li><li><a id="random-post" href="http://danbooru.donmai.us/posts/random">Random post</a></li>' + (postInfo.preview_file_url ? '<li><a href="http://danbooru.iqdb.org/db-search.php?url=http://danbooru.donmai.us' + postInfo.preview_file_url + '">Find similar</a></li>' : '') + '</ul>';
3878 }
3879
3880 function addRandomPostLink() {
3881 // Add the random post link and hotkey back to posts.
3882 var optionListItem = document.getElementById("add-to-pool-list") || document.getElementById("add-notes-list") || document.getElementById("add-artist-commentary-list");
3883
3884 if (!optionListItem || !add_random_post_link || gLoc !== "post")
3885 return;
3886
3887 // Create the link.
3888 var searchTags = getVar("tags");
3889
3890 var randomListItem = document.createElement("li");
3891 randomListItem.id = "random-post-list";
3892
3893 var randomLink = document.createElement("a");
3894 randomLink.id = "random-post";
3895 randomLink.href = "/posts/random" + (searchTags ? "?tags=" + searchTags : "");
3896 randomLink.innerHTML = "Random post";
3897 randomListItem.appendChild(randomLink);
3898
3899 optionListItem.parentNode.insertBefore(randomListItem, optionListItem);
3900
3901 // Create the hotkey.
3902 function randomHotkey() {
3903 location.href = randomLink.href;
3904 }
3905
3906 createHotkey("82", randomHotkey); // R
3907 }
3908
3909 function fixPostDownloadLinks() {
3910 // Fix the "size" and "download" links in the sidebar by creating the download link for logged out users (when able) and handling tagged filenames as necessary for all users.
3911 var postInfo = bbb.post.info;
3912 var i, il; // Loop variables.
3913
3914 // Fix the "size" link.
3915 var infoSection = document.getElementById("post-information");
3916
3917 if (infoSection) {
3918 var infoItems = infoSection.getElementsByTagName("li");
3919 var sizeRegex = /^(\s*Size:\s+)([\d\.]+\s+\S+)(\s+[\s\S]+)$/i;
3920
3921 for (i = 0, il = infoItems.length; i < il; i++) {
3922 var infoItem = infoItems[i];
3923 var infoText = infoItem.textContent.match(sizeRegex);
3924
3925 if (infoText) {
3926 infoItem.innerHTML = infoText[1] + '<a href="' + postInfo.file_img_src + '">' + infoText[2] + '</a>' + infoText[3];
3927 break;
3928 }
3929 }
3930 }
3931
3932 // Fix the "download" link.
3933 var optionsSection = document.getElementById("post-options");
3934
3935 if (optionsSection) {
3936 var optionItems = optionsSection.getElementsByTagName("li");
3937 var downloadRegex = /^\s*Download\s*$/i;
3938
3939 for (i = 0, il = optionItems.length; i < il; i++) {
3940 var optionItem = optionItems[i];
3941
3942 if (downloadRegex.test(optionItem.textContent)) {
3943 optionItem.innerHTML = '<a download="' + postInfo.tag_string_desc + " - " + postInfo.md5 + '.' + postInfo.file_ext + '" href="' + postInfo.file_img_src + '?download=1">Download</a>';
3944 break;
3945 }
3946 }
3947 }
3948 }
3949
3950 function modifyResizeLink() {
3951 // Replace the single resize link with three custom resize links.
3952 var resizeListLink = document.getElementById("image-resize-to-window-link");
3953
3954 if (!resizeListLink)
3955 return;
3956
3957 var resizeListItem = resizeListLink.parentNode;
3958 var resizeListParent = resizeListItem.parentNode;
3959 var optionsFrag = document.createDocumentFragment();
3960
3961 var resizeLinkAll = bbb.el.resizeLinkAll = document.createElement("a");
3962 resizeLinkAll.href = "#";
3963 resizeLinkAll.addEventListener("click", function(event) {
3964 if (event.button !== 0)
3965 return;
3966
3967 resizePost("all");
3968 event.preventDefault();
3969 }, false);
3970
3971 var resizeLinkWidth = bbb.el.resizeLinkWidth = document.createElement("a");
3972 resizeLinkWidth.href = "#";
3973 resizeLinkWidth.addEventListener("click", function(event) {
3974 if (event.button !== 0)
3975 return;
3976
3977 resizePost("width");
3978 event.preventDefault();
3979 }, false);
3980
3981 var resizeLinkHeight = bbb.el.resizeLinkHeight = document.createElement("a");
3982 resizeLinkHeight.href = "#";
3983 resizeLinkHeight.addEventListener("click", function(event) {
3984 if (event.button !== 0)
3985 return;
3986
3987 resizePost("height");
3988 event.preventDefault();
3989 }, false);
3990
3991 if (resize_link_style === "full") {
3992 var resizeListAll = document.createElement("li");
3993 optionsFrag.appendChild(resizeListAll);
3994
3995 resizeLinkAll.innerHTML = "Resize to window";
3996 resizeListAll.appendChild(resizeLinkAll);
3997
3998 var resizeListWidth = document.createElement("li");
3999 optionsFrag.appendChild(resizeListWidth);
4000
4001 resizeLinkWidth.innerHTML = "Resize to window width";
4002 resizeListWidth.appendChild(resizeLinkWidth);
4003
4004 var resizeListHeight = document.createElement("li");
4005 optionsFrag.appendChild(resizeListHeight);
4006
4007 resizeLinkHeight.innerHTML = "Resize to window height";
4008 resizeListHeight.appendChild(resizeLinkHeight);
4009
4010 resizeListParent.replaceChild(optionsFrag, resizeListItem);
4011 }
4012 else if (resize_link_style === "minimal") {
4013 var resizeList = document.createElement("li");
4014 optionsFrag.appendChild(resizeList);
4015
4016 var resizeLabelLink = document.createElement("a");
4017 resizeLabelLink.href = "#";
4018 resizeLabelLink.innerHTML = "Resize:";
4019 resizeLabelLink.style.marginRight = "2px";
4020 resizeLabelLink.addEventListener("click", function(event) {
4021 if (event.button !== 0)
4022 return;
4023
4024 if (bbb.post.resize.mode === "none")
4025 resizePost("all");
4026 else
4027 resizePost("none");
4028
4029 event.preventDefault();
4030 }, false);
4031 resizeList.appendChild(resizeLabelLink);
4032
4033 resizeLinkAll.innerHTML = "(W&H)";
4034 resizeLinkAll.className = "bbb-resize-link";
4035 resizeLinkAll.title = "Resize to Window Width & Height";
4036 resizeList.appendChild(resizeLinkAll);
4037
4038 resizeLinkWidth.innerHTML = "(W)";
4039 resizeLinkWidth.className = "bbb-resize-link";
4040 resizeLinkWidth.title = "Resize to Window Width";
4041 resizeList.appendChild(resizeLinkWidth);
4042
4043 resizeLinkHeight.innerHTML = "(H)";
4044 resizeLinkHeight.className = "bbb-resize-link";
4045 resizeLinkHeight.title = "Resize to Window Height";
4046 resizeList.appendChild(resizeLinkHeight);
4047
4048 resizeList.style.height = "0px";
4049 resizeList.style.visibility = "hidden";
4050 resizeList.style.fontWeight = "bold";
4051 resizeList.style.position = "relative";
4052
4053 resizeListParent.insertBefore(resizeList, resizeListItem);
4054
4055 var allWidth = resizeLinkAll.clientWidth;
4056 var widthWidth = resizeLinkWidth.clientWidth;
4057 var heightWidth = resizeLinkHeight.clientWidth;
4058
4059 resizeLinkAll.style.width = allWidth + "px";
4060 resizeLinkWidth.style.width = widthWidth + "px";
4061 resizeLinkHeight.style.width = heightWidth + "px";
4062
4063 resizeList.style.height = "auto";
4064 resizeList.style.visibility = "visible";
4065 resizeList.style.fontWeight = "normal";
4066
4067 resizeListParent.removeChild(resizeListItem);
4068 }
4069 }
4070
4071 function resizePost(mode) {
4072 // Custom resize post script.
4073 var postContent = getPostContent();
4074 var imgContainer = postContent.container;
4075 var contentDiv = document.getElementById("content");
4076 var ugoiraPanel = document.getElementById("ugoira-control-panel");
4077 var ugoiraSlider = document.getElementById("seek-slider");
4078 var target = postContent.el;
4079 var targetTag = (target ? target.tagName : undefined);
4080
4081 if (!target || !imgContainer || !contentDiv || targetTag === "A")
4082 return;
4083
4084 var currentMode = bbb.post.resize.mode;
4085 var currentRatio = bbb.post.resize.ratio;
4086 var resizeLinkAll = bbb.el.resizeLinkAll;
4087 var resizeLinkWidth = bbb.el.resizeLinkWidth;
4088 var resizeLinkHeight = bbb.el.resizeLinkHeight;
4089 var availableWidth = imgContainer.clientWidth || contentDiv.clientWidth - contentDiv.bbbGetPadding().width;
4090 var availableHeight = document.documentElement.clientHeight - 10;
4091 var targetCurrentWidth = target.clientWidth || parseFloat(target.style.width) || target.getAttribute("width");
4092 var targetCurrentHeight = target.clientHeight || parseFloat(target.style.height) || target.getAttribute("height");
4093 var useDataDim = targetTag === "EMBED" || targetTag === "VIDEO";
4094 var targetWidth = (useDataDim ? imgContainer.bbbInfo("width") : target.getAttribute("width")); // Was NOT expecting target.width to return the current width (css style width) and not the width attribute's value here...
4095 var targetHeight = (useDataDim ? imgContainer.bbbInfo("height") : target.getAttribute("height"));
4096 var tooWide = targetCurrentWidth > availableWidth;
4097 var tooTall = targetCurrentHeight > availableHeight;
4098 var widthRatio = availableWidth / targetWidth;
4099 var heightRatio = availableHeight / targetHeight;
4100 var imgMode = mode;
4101 var switchMode = false;
4102 var ratio = 1;
4103 var linkWeight = {all: "normal", width: "normal", height: "normal"};
4104
4105 if (mode === "swap") { // The image is being swapped between the original and sample image so everything needs to be reset. Ignore the current mode.
4106 switchMode = true;
4107 imgMode = "none";
4108 }
4109 else if (mode === currentMode || mode === "none" || (mode === "width" && widthRatio >= 1) || (mode === "height" && heightRatio >= 1) || (mode === "all" && widthRatio >= 1 && heightRatio >= 1)) { // Cases where resizing is being toggled off or isn't needed.
4110 if (currentMode !== "none") { // No need to do anything if the content is already at the original dimensions.
4111 switchMode = true;
4112 imgMode = "none";
4113 }
4114 }
4115 else if (mode === "height" && (tooTall || currentMode !== "none")) {
4116 switchMode = true;
4117 ratio = heightRatio;
4118 linkWeight.height = "bold";
4119 }
4120 else if (mode === "width" && (tooWide || currentMode !== "none")) {
4121 switchMode = true;
4122 ratio = widthRatio;
4123 linkWeight.width = "bold";
4124 }
4125 else if (mode === "all" && (tooWide || tooTall || currentMode !== "none")) {
4126 switchMode = true;
4127 ratio = (widthRatio < heightRatio ? widthRatio : heightRatio);
4128 linkWeight.all = "bold";
4129 }
4130
4131 if (switchMode) {
4132 if (currentRatio !== ratio || mode === "swap") {
4133 if (targetTag === "IMG" || targetTag === "CANVAS") {
4134 target.style.width = targetWidth * ratio + "px";
4135 target.style.height = targetHeight * ratio + "px";
4136
4137 if (ugoiraPanel && ugoiraSlider) {
4138 ugoiraPanel.style.width = targetWidth * ratio + "px";
4139 ugoiraSlider.style.width = targetWidth * ratio - 81 + "px";
4140 }
4141
4142 Danbooru.Note.Box.scale_all();
4143 }
4144 else if (targetTag === "EMBED") {
4145 var secondaryTarget = postContent.secEl;
4146
4147 secondaryTarget.height = target.height = targetHeight * ratio;
4148 secondaryTarget.width = target.width = targetWidth * ratio;
4149 }
4150 else if (targetTag === "VIDEO") {
4151 target.height = targetHeight * ratio;
4152 target.width = targetWidth * ratio;
4153 }
4154 }
4155
4156 bbb.post.resize.mode = imgMode;
4157 bbb.post.resize.ratio = ratio;
4158 resizeLinkAll.style.fontWeight = linkWeight.all;
4159 resizeLinkWidth.style.fontWeight = linkWeight.width;
4160 resizeLinkHeight.style.fontWeight = linkWeight.height;
4161 }
4162 }
4163
4164 function swapPost() {
4165 // Initiate the swap between the sample and original post content.
4166 var postInfo = bbb.post.info;
4167 var target = getPostContent().el;
4168 var targetTag = (target ? target.tagName : undefined);
4169 var bbbLoader = bbb.el.bbbLoader;
4170 var resizeStatus = bbb.el.resizeStatus;
4171 var resizeLink = bbb.el.resizeLink;
4172 var swapLink = bbb.el.swapLink;
4173
4174 if (!postInfo.has_large)
4175 return;
4176
4177 if (postInfo.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(postInfo.tag_string)) {
4178 if (targetTag === "CANVAS")
4179 location.href = updateURLQuery(location.href, {original: "0"});
4180 else if (targetTag === "VIDEO")
4181 location.href = updateURLQuery(location.href, {original: "1"});
4182 }
4183 else if (targetTag === "IMG") {
4184 if (image_swap_mode === "load") { // Load image and then view mode.
4185 if (bbbLoader.src !== "about:blank") { // Messages after cancelling.
4186 if (target.src.indexOf("/sample/") < 0)
4187 swapImageUpdate("original");
4188 else
4189 swapImageUpdate("sample");
4190
4191 bbbLoader.src = "about:blank";
4192 }
4193 else { // Messages during loading.
4194 if (target.src.indexOf("/sample/") < 0) {
4195 resizeStatus.innerHTML = "Loading sample image...";
4196 resizeLink.innerHTML = "cancel";
4197 swapLink.innerHTML = "View sample (cancel)";
4198 bbbLoader.src = postInfo.large_file_img_src;
4199 }
4200 else {
4201 resizeStatus.innerHTML = "Loading original image...";
4202 resizeLink.innerHTML = "cancel";
4203 swapLink.innerHTML = "View original (cancel)";
4204 bbbLoader.src = postInfo.file_img_src;
4205 }
4206 }
4207 }
4208 else if (image_swap_mode === "view") { // View image while loading mode.
4209 if (target.src.indexOf("/sample/") < 0) { // Load the sample image.
4210 swapImageUpdate("sample");
4211 target.src = "about:blank";
4212 target.removeAttribute("src");
4213 delayMe(function() { target.src = postInfo.large_file_img_src; });
4214 }
4215 else { // Load the original image.
4216 swapImageUpdate("original");
4217 target.src = "about:blank";
4218 target.removeAttribute("src");
4219 delayMe(function() { target.src = postInfo.file_img_src; });
4220 }
4221
4222 if (!bbb.post.swapped)
4223 delayMe(function() { resizePost("swap"); });
4224 else
4225 bbb.post.swapped = true;
4226 }
4227 }
4228 }
4229
4230 function swapImageUpdate(mode) {
4231 // Update all the elements related to swapping images when the image URL is changed.
4232 var postInfo = bbb.post.info;
4233 var img = document.getElementById("image");
4234 var bbbResizeNotice = bbb.el.resizeNotice;
4235 var resizeStatus = bbb.el.resizeStatus;
4236 var resizeLink = bbb.el.resizeLink;
4237 var swapLink = bbb.el.swapLink;
4238 var showResNot = bbb.user.show_resized_notice;
4239
4240 if (mode === "original") { // When the image is changed to the original image.
4241 resizeStatus.innerHTML = "Viewing original";
4242 resizeLink.innerHTML = "view sample";
4243 resizeLink.href = postInfo.large_file_img_src;
4244 swapLink.innerHTML = "View sample";
4245 swapLink.href = postInfo.large_file_img_src;
4246 img.setAttribute("height", postInfo.image_height);
4247 img.setAttribute("width", postInfo.image_width);
4248 bbbResizeNotice.style.display = (showResNot === "original" || showResNot === "all" ? "block" : "none");
4249 }
4250 else if (mode === "sample") { // When the image is changed to the sample image.
4251 resizeStatus.innerHTML = "Resized to " + Math.floor(postInfo.large_ratio * 100) + "% of original";
4252 resizeLink.innerHTML = "view original";
4253 resizeLink.href = postInfo.file_img_src;
4254 swapLink.innerHTML = "View original";
4255 swapLink.href = postInfo.file_img_src;
4256 img.setAttribute("height", postInfo.large_height);
4257 img.setAttribute("width", postInfo.large_width);
4258 bbbResizeNotice.style.display = (showResNot === "sample" || showResNot === "all" ? "block" : "none");
4259 }
4260 }
4261
4262 function checkRelations() {
4263 // Test whether the parent/child notice could have hidden posts.
4264 var postInfo = bbb.post.info;
4265 var loggedIn = isLoggedIn();
4266 var fixParent = false;
4267 var fixChild = false;
4268 var relationCookie = getCookie()["show-relationship-previews"];
4269 var showPreview = (relationCookie === undefined || relationCookie === "1");
4270 var parentLink = document.getElementById("has-children-relationship-preview-link");
4271 var childLink = document.getElementById("has-parent-relationship-preview-link");
4272 var thumbCount, deletedCount; // If/else variable.
4273
4274 if (postInfo.has_children) {
4275 var parentNotice = document.getElementsByClassName("notice-parent")[0];
4276
4277 if (parentNotice) {
4278 var parentText = parentNotice.textContent.match(/has (\d+|a) child/);
4279 var parentCount = (parentText ? Number(parentText[1]) || 1 : 0);
4280 thumbCount = getPosts(parentNotice).length;
4281 deletedCount = parentNotice.getElementsByClassName("post-status-deleted").length;
4282
4283 if ((!loggedIn && show_deleted && !deletedCount) || (parentCount && parentCount + 1 !== thumbCount))
4284 fixParent = true;
4285 }
4286 else if (show_deleted)
4287 fixParent = true;
4288 }
4289
4290 if (fixParent) {
4291 if (showPreview || !parentLink)
4292 searchJSON("parent", postInfo.id);
4293 else
4294 parentLink.addEventListener("click", requestRelations, false);
4295 }
4296
4297 if (postInfo.parent_id) {
4298 var childNotice = document.getElementsByClassName("notice-child")[0];
4299
4300 if (childNotice) {
4301 var childText = childNotice.textContent.match(/has (\d+|a) sibling/);
4302 var childCount = (childText ? Number(childText[1]) || 1 : 0) + 1;
4303 thumbCount = getPosts(childNotice).length;
4304 deletedCount = childNotice.getElementsByClassName("post-status-deleted").length;
4305
4306 if ((!loggedIn && show_deleted && !deletedCount) || (childCount && childCount + 1 !== thumbCount))
4307 fixChild = true;
4308 }
4309 }
4310
4311 if (fixChild) {
4312 if (showPreview || !childLink)
4313 searchJSON("child", postInfo.parent_id);
4314 else
4315 childLink.addEventListener("click", requestRelations, false);
4316 }
4317 }
4318
4319 function requestRelations(event) {
4320 // Start the parent/child notice JSON request when the user chooses to display the thumbs in a notice.
4321 if (event.button !== 0)
4322 return;
4323
4324 var postInfo = bbb.post.info;
4325 var target = event.target;
4326
4327 if (target.id === "has-children-relationship-preview-link")
4328 searchJSON("parent", postInfo.id);
4329 else if (target.id === "has-parent-relationship-preview-link")
4330 searchJSON("child", postInfo.parent_id);
4331
4332 target.removeEventListener("click", requestRelations, false);
4333 event.preventDefault();
4334 }
4335
4336 function removeTagHeaders() {
4337 // Remove the "copyright", "characters", and "artist" headers in the post sidebar.
4338 var tagList = document.getElementById("tag-list");
4339
4340 if (!tagList || !remove_tag_headers || gLoc !== "post")
4341 return;
4342
4343 var tagHolder = document.createDocumentFragment();
4344 var childIndex = 0;
4345 var mainList; // If/else variable.
4346
4347 while (tagList.children[childIndex]) {
4348 var header = tagList.children[childIndex];
4349 var list = tagList.children[childIndex + 1];
4350
4351 if (header.tagName === "H2" && list && list.tagName === "UL") {
4352 tagList.removeChild(header);
4353 tagList.removeChild(list);
4354
4355 while (list.firstElementChild)
4356 tagHolder.appendChild(list.firstElementChild);
4357 }
4358 else if (header.tagName === "H1" && list && list.tagName === "UL") {
4359 mainList = list;
4360 childIndex += 2;
4361 }
4362 else
4363 childIndex++;
4364 }
4365
4366 if (mainList)
4367 mainList.insertBefore(tagHolder, mainList.firstElementChild);
4368 else {
4369 var newHeader = document.createElement("h1");
4370 newHeader.innerHTML = "Tags";
4371 tagList.appendChild(newHeader);
4372
4373 var newList = document.createElement("ul");
4374 newList.appendChild(tagHolder);
4375 tagList.appendChild(newList);
4376 }
4377 }
4378
4379 function postTagTitles() {
4380 // Replace the post title with the full set of tags.
4381 if (post_tag_titles && gLoc === "post")
4382 document.title = document.bbbInfo("tags").replace(/\s/g, ", ").replace(/_/g, " ") + " - Danbooru";
4383 }
4384
4385 function minimizeStatusNotices() {
4386 // Show status notices only when their respective status link is clicked in the sidebar.
4387 if (!minimize_status_notices || gLoc !== "post")
4388 return;
4389
4390 var infoSection = document.getElementById("post-information");
4391 var infoListItems = (infoSection ? infoSection.getElementsByTagName("li") : null);
4392 var flaggedNotice = document.getElementsByClassName("notice-flagged")[0];
4393 var appealedNotice = document.getElementsByClassName("notice-appealed")[0];
4394 var pendingNotice = document.getElementsByClassName("notice-pending")[0];
4395 var deletedNotices = document.getElementsByClassName("notice-deleted");
4396 var i, il, statusListItem, newStatusContent, deletedNotice, bannedNotice; // Loop variables.
4397
4398 if (infoListItems) {
4399 // Locate the status portion of the information section.
4400 for (i = infoListItems.length - 1; i >= 0; i--) {
4401 var infoListItem = infoListItems[i];
4402
4403 if (infoListItem.textContent.indexOf("Status:") > -1) {
4404 statusListItem = infoListItem;
4405 newStatusContent = statusListItem.textContent;
4406 break;
4407 }
4408 }
4409
4410 // Hide and alter the notices and create the appropriate status links.
4411 if (statusListItem) {
4412 if (flaggedNotice) {
4413 flaggedNotice.style.display = "none";
4414 flaggedNotice.style.position = "absolute";
4415 flaggedNotice.style.zIndex = "2003";
4416 newStatusContent = newStatusContent.replace("Flagged", '<a href="#" id="bbb-flagged-link">Flagged</a>');
4417 }
4418
4419 if (pendingNotice) {
4420 pendingNotice.style.display = "none";
4421 pendingNotice.style.position = "absolute";
4422 pendingNotice.style.zIndex = "2003";
4423 newStatusContent = newStatusContent.replace("Pending", '<a href="#" id="bbb-pending-link">Pending</a>');
4424 }
4425
4426 for (i = 0, il = deletedNotices.length; i < il; i++) {
4427 deletedNotices[i].style.display = "none";
4428 deletedNotices[i].style.position = "absolute";
4429 deletedNotices[i].style.zIndex = "2003";
4430
4431 if (deletedNotices[i].textContent.indexOf("This post was deleted") > -1) {
4432 deletedNotice = deletedNotices[i];
4433 newStatusContent = newStatusContent.replace("Deleted", '<a href="#" id="bbb-deleted-link">Deleted</a>');
4434 }
4435 else {
4436 bannedNotice = deletedNotices[i];
4437 newStatusContent = newStatusContent.replace("Banned", '<a href="#" id="bbb-banned-link">Banned</a>');
4438 }
4439 }
4440
4441 if (appealedNotice) {
4442 appealedNotice.style.display = "none";
4443 appealedNotice.style.position = "absolute";
4444 appealedNotice.style.zIndex = "2003";
4445 newStatusContent = newStatusContent + ' <a href="#" id="bbb-appealed-link">Appealed</a>';
4446 }
4447
4448 statusListItem.innerHTML = newStatusContent;
4449 }
4450
4451 // Prepare the links.
4452 var flaggedLink = document.getElementById("bbb-flagged-link");
4453 var appealedLink = document.getElementById("bbb-appealed-link");
4454 var pendingLink = document.getElementById("bbb-pending-link");
4455 var deletedLink = document.getElementById("bbb-deleted-link");
4456 var bannedLink = document.getElementById("bbb-banned-link");
4457
4458 if (flaggedLink)
4459 statusLinkEvents(flaggedLink, flaggedNotice);
4460 if (appealedLink)
4461 statusLinkEvents(appealedLink, appealedNotice);
4462 if (pendingLink)
4463 statusLinkEvents(pendingLink, pendingNotice);
4464 if (deletedLink)
4465 statusLinkEvents(deletedLink, deletedNotice);
4466 if (bannedLink)
4467 statusLinkEvents(bannedLink, bannedNotice);
4468 }
4469 }
4470
4471 function statusLinkEvents(link, notice) {
4472 // Attach events to the status links to enable a tooltip style notice.
4473 link.addEventListener("click", function(event) {
4474 if (event.button === 0)
4475 showStatusNotice(event, notice);
4476 }, false);
4477 link.addEventListener("mouseout", function() {
4478 bbb.timers.minNotice = window.setTimeout(function() {
4479 notice.style.display = "none";
4480 }, 200);
4481 }, false);
4482 notice.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.minNotice); }, false);
4483 notice.addEventListener("mouseleave", function() { notice.style.display = "none"; }, false);
4484 }
4485
4486 function showStatusNotice(event, noticeEl) {
4487 // Display a minimized status notice upon a click event.
4488 var x = event.pageX;
4489 var y = event.pageY;
4490 var notice = noticeEl;
4491 var topOffset = 0;
4492
4493 notice.style.maxWidth = document.documentElement.clientWidth * 0.66 + "px";
4494 notice.style.visibility = "hidden";
4495 notice.style.display = "block";
4496
4497 // Don't allow the notice to go above the top of the window.
4498 if (event.clientY - notice.offsetHeight - 2 < 5)
4499 topOffset = event.clientY - notice.offsetHeight - 7;
4500
4501 notice.style.left = x + 2 + "px";
4502 notice.style.top = y - notice.offsetHeight - 2 - topOffset + "px";
4503 notice.style.visibility = "visible";
4504
4505 event.preventDefault();
4506 }
4507
4508 function dragScrollInit() {
4509 // Start up drag scroll.
4510 if (!post_drag_scroll)
4511 return;
4512
4513 var target = getPostContent().el;
4514 var targetTag = (target ? target.tagName : undefined);
4515
4516 if (targetTag === "IMG" || targetTag === "VIDEO" || targetTag === "CANVAS") {
4517 bbb.drag_scroll.target = target;
4518
4519 if (!Danbooru.Note.TranslationMode.active)
4520 dragScrollEnable();
4521
4522 var startFunction = Danbooru.Note.TranslationMode.start;
4523 var stopFunction = Danbooru.Note.TranslationMode.stop;
4524
4525 Danbooru.Note.TranslationMode.start = function(event) {
4526 startFunction(event);
4527 dragScrollToggle();
4528 };
4529
4530 Danbooru.Note.TranslationMode.stop = function(event) {
4531 stopFunction(event);
4532 dragScrollToggle();
4533 };
4534
4535 // Disable click behavior when dragging the video around.
4536 if (targetTag === "VIDEO") {
4537 target.parentNode.addEventListener("click", function(event) {
4538 if (event.button === 0 && event.target.id === "image" && bbb.drag_scroll.moved)
4539 event.preventDefault();
4540 }, true);
4541 }
4542 }
4543 }
4544
4545 function dragScrollToggle() {
4546 // Enable drag scroll with translation mode is off and disable it when translation mode is on.
4547 if (!post_drag_scroll || !bbb.drag_scroll.target)
4548 return;
4549
4550 if (Danbooru.Note.TranslationMode.active)
4551 dragScrollDisable();
4552 else
4553 dragScrollEnable();
4554 }
4555
4556 function dragScrollEnable() {
4557 // Add the drag scroll event listeners.
4558 var target = bbb.drag_scroll.target;
4559
4560 target.addEventListener("mousedown", dragScrollOn, false);
4561 target.addEventListener("dragstart", disableEvent, false);
4562 target.addEventListener("selectstart", disableEvent, false);
4563 }
4564
4565 function dragScrollDisable() {
4566 // Remove the drag scroll event listeners.
4567 var target = bbb.drag_scroll.target;
4568
4569 target.removeEventListener("mousedown", dragScrollOn, false);
4570 target.removeEventListener("dragstart", disableEvent, false);
4571 target.removeEventListener("selectstart", disableEvent, false);
4572 }
4573
4574 function dragScrollOn(event) {
4575 // Start monitoring mouse movement.
4576 if (event.button === 0) {
4577 bbb.drag_scroll.lastX = event.clientX;
4578 bbb.drag_scroll.lastY = event.clientY;
4579 bbb.drag_scroll.moved = false;
4580
4581 document.addEventListener("mousemove", dragScrollMove, false);
4582 document.addEventListener("mouseup", dragScrollOff, false);
4583 }
4584 }
4585
4586 function dragScrollMove(event) {
4587 // Move the page based on mouse movement.
4588 var newX = event.clientX;
4589 var newY = event.clientY;
4590 var xDistance = bbb.drag_scroll.lastX - newX;
4591 var yDistance = bbb.drag_scroll.lastY - newY;
4592
4593 window.scrollBy(xDistance, yDistance);
4594
4595 bbb.drag_scroll.lastX = newX;
4596 bbb.drag_scroll.lastY = newY;
4597 bbb.drag_scroll.moved = xDistance !== 0 || yDistance !== 0 || bbb.drag_scroll.moved; // Doing this since I'm not sure what Chrome's mousemove event is doing. It apparently fires even when the moved distance is equal to zero.
4598 }
4599
4600 function dragScrollOff() {
4601 // Stop monitoring mouse movement.
4602 document.removeEventListener("mousemove", dragScrollMove, false);
4603 document.removeEventListener("mouseup", dragScrollOff, false);
4604 }
4605
4606 function disableEvent(event) {
4607 // removeEventListener friendly function for stopping an event.
4608 event.preventDefault();
4609 }
4610
4611 function autoscrollPost() {
4612 // Automatically scroll a post to the desired position.
4613 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
4614
4615 if (autoscroll_post === "none" || scrolled !== 0) // Don't scroll if the page is already srolled.
4616 return;
4617
4618 if (autoscroll_post === "post") {
4619 var target = getPostContent().el;
4620
4621 if (target)
4622 target.scrollIntoView();
4623 }
4624 else if (autoscroll_post === "header") {
4625 var page = document.getElementById("page");
4626
4627 if (!page)
4628 return;
4629
4630 var pageTop = page.offsetTop;
4631
4632 window.scroll(0, pageTop);
4633 }
4634 }
4635
4636 function videoVolume() {
4637 // Set the volume of video posts to a specified level.
4638 var vid = document.getElementById("image");
4639
4640 if (video_volume === "disabled" || !vid || vid.tagName !== "VIDEO")
4641 return;
4642
4643 if (video_volume === "muted")
4644 vid.muted = true;
4645 else if (video_volume === "remember") {
4646 vid.volume = video_volume_data.level;
4647 vid.muted = video_volume_data.muted;
4648
4649 vid.addEventListener("volumechange", function() {
4650 // Save volume changes half a second after changes stop.
4651 if (bbb.timers.saveVideoVolume)
4652 window.clearTimeout(bbb.timers.saveVideoVolume);
4653
4654 bbb.timers.saveVideoVolume = window.setTimeout( function() {
4655 loadSettings();
4656 bbb.user.video_volume_data.level = vid.volume;
4657 bbb.user.video_volume_data.muted = vid.muted;
4658 bbb.timers.saveVideoVolume = 0;
4659 saveSettings();
4660 }, 500);
4661 }, false);
4662 }
4663 else // Percent values.
4664 vid.volume = video_volume;
4665 }
4666
4667 /* Thumbnail functions */
4668 function formatThumbnails(target) {
4669 // Create thumbnail titles and borders.
4670 var posts = getPosts(target);
4671 var i, il; // Loop variables.
4672
4673 if (!posts[0])
4674 return;
4675
4676 var searches = bbb.custom_tag.searches;
4677
4678 // Create and cache border search objects.
4679 if (custom_tag_borders && !searches[0]) {
4680 for (i = 0, il = tag_borders.length; i < il; i++)
4681 searches.push(createSearch(tag_borders[i].tags));
4682 }
4683
4684 // Cycle through each post and apply titles and borders.
4685 for (i = 0, il = posts.length; i < il; i++) {
4686 var post = posts[i];
4687 var postSibling = post.nextSibling;
4688 var img = post.getElementsByTagName("img")[0];
4689
4690 // Clean out text nodes between thumbnail articles.
4691 if (postSibling && postSibling.nodeType === 3)
4692 postSibling.parentNode.removeChild(postSibling);
4693
4694 if (!img)
4695 continue;
4696
4697 var postInfo = post.bbbInfo();
4698 var link = img.bbbParent("A", 3);
4699 var tagsStr = postInfo.tag_string || "";
4700 var userStr = (postInfo.uploader_name ? " user:" + postInfo.uploader_name : "");
4701 var ratingStr = (postInfo.rating ? " rating:" + postInfo.rating : "");
4702 var scoreStr = (typeof(postInfo.score) === "number" ? " score:" + postInfo.score : "");
4703 var titleStr = tagsStr + userStr + ratingStr + scoreStr;
4704 var titleAttr = (getMeta("disable-post-tooltips") === "false" ? "oldtitle" : "title");
4705 var secondary = [];
4706 var secondaryLength = 0;
4707 var styleList = bbb.custom_tag.style_list;
4708 var borderStyle; // If/else variable.
4709
4710 // Skip thumbnails that have already been done.
4711 if (link.bbbHasClass("bbb-thumb-link"))
4712 continue;
4713
4714 // Create title information.
4715 img.setAttribute(titleAttr, titleStr);
4716
4717 // Add custom data attributes.
4718 post.bbbInfo("file-url-desc", postInfo.file_url_desc);
4719
4720 // Give the thumbnail link an identifying class.
4721 link.bbbAddClass("bbb-thumb-link");
4722
4723 // Give the post container an ID class for resolving cases where the same post shows up on the page multiple times.
4724 post.bbbAddClass("post_" + postInfo.id);
4725
4726 // Correct parent status borders on "no active children" posts for logged out users.
4727 if (postInfo.has_children && show_deleted)
4728 post.bbbAddClass("post-status-has-children");
4729
4730 // Secondary custom tag borders.
4731 if (custom_tag_borders) {
4732 if (typeof(styleList[postInfo.id]) === "undefined") {
4733 for (var j = 0, jl = tag_borders.length; j < jl; j++) {
4734 var tagBorderItem = tag_borders[j];
4735
4736 if (tagBorderItem.is_enabled && thumbSearchMatch(post, searches[j])) {
4737 secondary.push([tagBorderItem.border_color, tagBorderItem.border_style]);
4738
4739 if (secondary.length === 4)
4740 break;
4741 }
4742 }
4743
4744 secondaryLength = secondary.length;
4745
4746 if (secondaryLength) {
4747 link.bbbAddClass("bbb-custom-tag");
4748
4749 if (secondaryLength === 1 || (single_color_borders && secondaryLength > 1))
4750 borderStyle = "border-color: " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " !important;";
4751 else if (secondaryLength === 2)
4752 borderStyle = "border-color: " + secondary[0][0] + " " + secondary[1][0] + " " + secondary[1][0] + " " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " " + secondary[1][1] + " " + secondary[1][1] + " " + secondary[0][1] + " !important;";
4753 else if (secondaryLength === 3)
4754 borderStyle = "border-color: " + secondary[0][0] + " " + secondary[1][0] + " " + secondary[2][0] + " " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " " + secondary[1][1] + " " + secondary[2][1] + " " + secondary[0][1] + " !important;";
4755 else if (secondaryLength === 4)
4756 borderStyle = "border-color: " + secondary[0][0] + " " + secondary[2][0] + " " + secondary[3][0] + " " + secondary[1][0] + " !important; border-style: " + secondary[0][1] + " " + secondary[2][1] + " " + secondary[3][1] + " " + secondary[1][1] + " !important;";
4757
4758 link.setAttribute("style", borderStyle);
4759 styleList[postInfo.id] = borderStyle;
4760 }
4761 else
4762 styleList[postInfo.id] = false;
4763 }
4764 else if (styleList[postInfo.id] !== false && !post.bbbHasClass("bbb-custom-tag")) { // Post is already tested, but needs to be set up again.
4765 link.bbbAddClass("bbb-custom-tag");
4766 link.setAttribute("style", styleList[postInfo.id]);
4767 }
4768 }
4769 }
4770 }
4771
4772 function prepThumbnails(target) {
4773 // Take new thumbnails and apply the necessary functions for preparing them.
4774 // Thumbnail classes and titles.
4775 formatThumbnails(target);
4776
4777 // Thumbnail info.
4778 thumbInfo(target);
4779
4780 // Fix post link queries.
4781 postLinkQuery(target);
4782
4783 // Blacklist.
4784 blacklistUpdate(target);
4785
4786 // Direct downloads.
4787 postDDL(target);
4788
4789 // Quick search.
4790 quickSearchTest(target);
4791
4792 // Fix the mode menu.
4793 danbModeMenu(target);
4794 }
4795
4796 function createThumbHTML(postInfo, query) {
4797 // Create a thumbnail HTML string.
4798 return '<article class="post-preview' + postInfo.thumb_class + '" id="post_' + postInfo.id + '" data-id="' + postInfo.id + '" data-has-sound="' + postInfo.has_sound + '" data-tags="' + postInfo.tag_string + '" data-pools="' + postInfo.pool_string + '" data-uploader="' + postInfo.uploader_name + '" data-rating="' + postInfo.rating + '" data-width="' + postInfo.image_width + '" data-height="' + postInfo.image_height + '" data-flags="' + postInfo.flags + '" data-parent-id="' + postInfo.parent_id + '" data-has-children="' + postInfo.has_children + '" data-score="' + postInfo.score + '" data-fav-count="' + postInfo.fav_count + '" data-approver-id="' + postInfo.approver_id + '" data-pixiv-id="' + postInfo.pixiv_id + '" data-md5="' + postInfo.md5 + '" data-file-ext="' + postInfo.file_ext + '" data-file-url="' + postInfo.file_url + '" data-large-file-url="' + postInfo.large_file_url + '" data-preview-file-url="' + postInfo.preview_file_url + '" data-source="' + postInfo.source + '" data-top-tagger="' + postInfo.keeper_data.uid + '" data-uploader-id="' + postInfo.uploader_id + '" data-normalized-source="' + postInfo.normalized_source + '" data-is-favorited="' + postInfo.is_favorited + '" data-file-url-desc="' + postInfo.file_url_desc + '"><a href="/posts/' + postInfo.id + query + '"><picture><source media="(max-width: 660px)" srcset="' + postInfo.crop_img_src + '"><source media="(min-width: 660px)" srcset="' + postInfo.preview_img_src + '"><img src="' + postInfo.preview_img_src + '" alt="' + postInfo.tag_string + '"></picture></a></article>';
4799 }
4800
4801 function createThumb(postInfo, query) {
4802 // Create a thumbnail element. (lazy method <_<)
4803 var childSpan = document.createElement("span");
4804 childSpan.innerHTML = createThumbHTML(postInfo, query);
4805
4806 return childSpan.firstElementChild;
4807 }
4808
4809 function createThumbListing(postsInfo, orderedIds) {
4810 // Create a listing of thumbnails.
4811 var thumbs = document.createDocumentFragment();
4812 var postHolder = {};
4813 var query = getThumbQuery();
4814 var i, il, thumb; // Loop variables;
4815
4816 // Generate thumbnails.
4817 for (i = 0, il = postsInfo.length; i < il; i++) {
4818 var postInfo = postsInfo[i];
4819
4820 // Don't display loli/shota/toddlercon/deleted/banned if the user has opted so and skip to the next image.
4821 if ((!show_loli && /(?:^|\s)loli(?:$|\s)/.test(postInfo.tag_string)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(postInfo.tag_string)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(postInfo.tag_string)) || (!show_deleted && postInfo.is_deleted) || (!show_banned && postInfo.is_banned) || safebPostTest(postInfo))
4822 continue;
4823
4824 // eek, not so huge line.
4825 thumb = createThumb(postInfo, query);
4826
4827 // Generate output.
4828 if (!orderedIds)
4829 thumbs.appendChild(thumb);
4830 else
4831 postHolder[postInfo.id] = thumb;
4832 }
4833
4834 // Place thumbnails in the correct order for pools.
4835 if (orderedIds) {
4836 for (i = 0, il = orderedIds.length; i < il; i++) {
4837 thumb = postHolder[orderedIds[i]];
4838
4839 if (thumb)
4840 thumbs.appendChild(thumb);
4841 }
4842 }
4843
4844 return thumbs;
4845 }
4846
4847 function updateThumbListing(thumbs) {
4848 // Take a collection of thumbnails and use them to update the original thumbnail listing as appropriate.
4849 var thumbContainer = getThumbContainer(gLoc);
4850 var before = getThumbSibling(gLoc);
4851 var newContainer; // If/else variable.
4852
4853 if (!thumbContainer) {
4854 bbbNotice("Thumbnail section could not be located.", -1);
4855 return;
4856 }
4857
4858 if ((history.state && history.state.bbb_posts_cache) || !isRandomSearch()) {
4859 // New thumbnail container replacement preparation.
4860 var childIndex = 0;
4861
4862 newContainer = thumbContainer.cloneNode(false);
4863
4864 while (thumbContainer.children[childIndex]) {
4865 var child = thumbContainer.children[childIndex];
4866
4867 if (child.tagName !== "ARTICLE")
4868 newContainer.appendChild(child);
4869 else
4870 childIndex++;
4871 }
4872
4873 if (!before)
4874 newContainer.appendChild(thumbs);
4875 else
4876 newContainer.insertBefore(thumbs, before);
4877
4878 // Prepare thumbnails.
4879 prepThumbnails(newContainer);
4880
4881 // Replace results with new results.
4882 thumbContainer.parentNode.replaceChild(newContainer, thumbContainer);
4883 }
4884 else {
4885 // Fill out a random search by appending thumbnails.
4886 var origThumbs = getPosts(thumbs);
4887 var i, il, curThumb; // Loop variables.
4888
4889 newContainer = document.createDocumentFragment();
4890
4891 // Remove existing posts.
4892 for (i = 0, il = origThumbs.length; i < il; i++) {
4893 curThumb = origThumbs[i];
4894
4895 if (getId(curThumb.id))
4896 thumbs.removeChild(curThumb);
4897 }
4898
4899 // Favor hidden posts since they're the most likely reason for the API request.
4900 var noDupThumbs = getPosts(thumbs);
4901 var hiddenSearch = createSearch("~loli ~shota ~toddlercon ~status:deleted ~status:banned");
4902 var limit = getLimit() || (allowUserLimit() ? thumbnail_count : thumbnail_count_default);
4903 var numMissing = limit - getPosts().length;
4904
4905 for (i = 0, il = noDupThumbs.length; i < il; i++) {
4906 curThumb = noDupThumbs[i];
4907
4908 if (numMissing === 0)
4909 break;
4910 else if (thumbSearchMatch(curThumb, hiddenSearch)) {
4911 newContainer.appendChild(curThumb);
4912 numMissing--;
4913 }
4914 }
4915
4916 // Try to fix any shortage of thumbnails.
4917 var leftoverThumbs = getPosts(thumbs);
4918
4919 for (i = 0, il = leftoverThumbs.length; i < il; i++) {
4920 if (numMissing === 0)
4921 break;
4922 else {
4923 newContainer.appendChild(leftoverThumbs[i]);
4924 numMissing--;
4925 }
4926 }
4927
4928 // Prepare thumbnails.
4929 prepThumbnails(newContainer);
4930
4931 // Append listing with new thumbnails.
4932 if (!before)
4933 thumbContainer.appendChild(newContainer);
4934 else
4935 thumbContainer.insertBefore(newContainer, before);
4936 }
4937 }
4938
4939 function getIdCache() {
4940 // Retrieve the cached list of post IDs used for the pool/favorite group thumbnails.
4941 var collId = location.href.match(/\/(?:pools|favorite_groups)\/(\d+)/)[1];
4942 var idCache = sessionStorage.getItem("bbb_" + gLoc + "_cache_" + collId);
4943 var curTime = new Date().getTime();
4944 var cacheTime, timeDiff; // If/else variables.
4945
4946 if (idCache) {
4947 idCache = idCache.split(" ");
4948 cacheTime = idCache.shift();
4949 timeDiff = (curTime - cacheTime) / 1000; // Cache age in seconds.
4950 }
4951
4952 if (!idCache || (timeDiff && timeDiff > 600))
4953 return undefined;
4954 else
4955 return idCache.join(" ");
4956 }
4957
4958 function thumbInfo(target) {
4959 // Add score, favorite count, and rating info to thumbnails.
4960 var posts = getPosts(target);
4961
4962 if (thumb_info === "disabled")
4963 return;
4964
4965 for (var i = 0, il = posts.length; i < il; i++) {
4966 var post = posts[i];
4967
4968 // Skip thumbnails that already have the info added.
4969 if (post.getElementsByClassName("bbb-thumb-info")[0])
4970 continue;
4971
4972 var postInfo = post.bbbInfo();
4973 var tooShort = (150 / postInfo.image_width * postInfo.image_height < 30); // Short thumbnails will need the info div position adjusted.
4974
4975 if (gLoc === "comments") { // Add favorites info to the existing info in the comments listing.
4976 var firstInfo = post.getElementsByClassName("info")[0];
4977 var infoParent = (firstInfo ? firstInfo.parentNode : undefined);
4978
4979 if (infoParent) {
4980 var favSpan = document.createElement("span");
4981 favSpan.className = "info bbb-thumb-info";
4982 favSpan.innerHTML = '<strong>Favorites</strong> ' + postInfo.fav_count;
4983 infoParent.appendChild(favSpan);
4984 }
4985 }
4986 else { // Add extra information inside of the thumbnail's parent element.
4987 var thumbImg = post.getElementsByTagName("img")[0];
4988
4989 // Don't add the info if there isn't a thumbnail.
4990 if (!thumbImg)
4991 continue;
4992
4993 var thumbEl = post.getElementsByClassName("preview")[0] || post;
4994 thumbEl.bbbAddClass("bbb-thumb-info-parent");
4995
4996 var postLink = thumbEl.getElementsByTagName("a")[0];
4997 var before = (postLink ? postLink.nextElementSibling : undefined);
4998 var scoreStr = (postInfo.score < 0 ? '<span style="color: #CC0000;">' + postInfo.score + '</span>' : postInfo.score);
4999
5000 var infoDiv = document.createElement("div");
5001 infoDiv.className = "bbb-thumb-info" + (tooShort ? " bbb-thumb-info-short" : "");
5002 infoDiv.innerHTML = "★" + scoreStr + " ♥" + postInfo.fav_count + (location.host.indexOf("safebooru") < 0 ? " " + postInfo.rating.toUpperCase() : "");
5003
5004 if (before)
5005 thumbEl.insertBefore(infoDiv, before);
5006 else
5007 thumbEl.appendChild(infoDiv);
5008 }
5009 }
5010 }
5011
5012 function postDDL(target) {
5013 // Add direct downloads to thumbnails.
5014 if (!direct_downloads || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorites" && gLoc !== "favorite_group"))
5015 return;
5016
5017 var posts = getPosts(target);
5018
5019 for (var i = 0, il = posts.length; i < il; i++) {
5020 var post = posts[i];
5021 var postInfo = post.bbbInfo();
5022 var postUrl = (postInfo.large_file_img_src.indexOf(".webm") > -1 ? postInfo.large_file_img_src : postInfo.file_img_src);
5023 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
5024
5025 // If the direct download doesn't already exist, create it.
5026 if (!ddlLink) {
5027 ddlLink = document.createElement("a");
5028 ddlLink.innerHTML = "Direct Download";
5029 ddlLink.className = "bbb-ddl";
5030 post.appendChild(ddlLink);
5031 }
5032
5033 ddlLink.href = postUrl || "/data/DDL unavailable for post " + postInfo.id + ".jpg";
5034
5035 // Disable filtered posts.
5036 if (post.bbbHasClass("blacklisted-active", "bbb-quick-search-filtered"))
5037 unsetDDL(ddlLink);
5038 }
5039 }
5040
5041 function enablePostDDL(post) {
5042 // Enable a post's DDL.
5043 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
5044
5045 if (!direct_downloads || !ddlLink || post.bbbHasClass("blacklisted-active", "bbb-quick-search-filtered"))
5046 return;
5047
5048 ddlLink.href = ddlLink.href.replace("donmai.us/#data", "donmai.us/data");
5049 }
5050
5051 function disablePostDDL(post) {
5052 // Disable a post's DDL.
5053 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
5054
5055 if (!direct_downloads || !ddlLink)
5056 return;
5057
5058 unsetDDL(ddlLink);
5059 }
5060
5061 function unsetDDL(ddlLink) {
5062 // Disable a DDL URL with an anchor.
5063 ddlLink.href = ddlLink.href.replace("donmai.us/data", "donmai.us/#data");
5064 }
5065
5066 function postLinkQuery(target) {
5067 // Remove or add the query portion of links to posts.
5068 var addQuery = !isLoggedIn();
5069 var removeQuery = clean_links;
5070
5071 if (!removeQuery && !addQuery)
5072 return;
5073
5074 var targetContainer; // If/else variable.
5075
5076 if (target)
5077 targetContainer = target;
5078 else if (gLoc === "post")
5079 targetContainer = document.getElementById("content");
5080 else if (gLoc === "pool" || gLoc === "favorite_group") {
5081 targetContainer = document.getElementById("a-show");
5082 targetContainer = (targetContainer ? targetContainer.getElementsByTagName("section")[0] : undefined);
5083 }
5084 else if (gLoc === "search" || gLoc === "favorites")
5085 targetContainer = document.getElementById("posts");
5086 else if (gLoc === "intro")
5087 targetContainer = document.getElementById("a-intro");
5088
5089 if (targetContainer) {
5090 var links = targetContainer.getElementsByTagName("a");
5091 var i, il; // Loop variables.
5092
5093 if (removeQuery) {
5094 // Remove the query portion of links to posts.
5095 for (i = 0, il = links.length; i < il; i++) {
5096 var remLink = links[i];
5097 var remLinkParent = remLink.parentNode;
5098
5099 if (remLinkParent.tagName === "ARTICLE" || remLinkParent.id.indexOf("nav-link-for-pool-") === 0)
5100 remLink.href = remLink.href.split("?", 1)[0];
5101 }
5102 }
5103 else if (addQuery) {
5104 // Add the query portion of links to posts for logged out users.
5105 var tagQuery = getVar("tags");
5106
5107 if (tagQuery) {
5108 for (i = 0, il = links.length; i < il; i++) {
5109 var addLink = links[i];
5110 var addLinkParent = addLink.parentNode;
5111 var addLinkHref = addLink.href;
5112
5113 if (addLinkParent.tagName === "ARTICLE" && !getVar("tags", addLinkHref))
5114 addLink.href = updateURLQuery(addLinkHref, {tags: tagQuery});
5115 }
5116 }
5117 }
5118 }
5119 }
5120
5121 function danbModeMenu(target) {
5122 // Add mode menu functionality to newly created thumbnails.
5123 var modeSection = document.getElementById("mode-box");
5124
5125 if (!modeSection)
5126 return;
5127
5128 var links = (target || document).getElementsByClassName("bbb-thumb-link");
5129 var menuHandler = function(event) {
5130 if (event.button === 0)
5131 Danbooru.PostModeMenu.click(event);
5132 };
5133
5134 for (var i = 0, il = links.length; i < il; i++)
5135 links[i].addEventListener("click", menuHandler, false);
5136 }
5137
5138 function potentialHiddenPosts(mode, target) {
5139 // Check a normal thumbnail listing for possible hidden posts.
5140 var numPosts = getPosts(target).length;
5141 var noResults = noResultsPage(target);
5142 var limit = getLimit();
5143
5144 if (mode === "search" || mode === "favorites") {
5145 var numExpected = (limit !== undefined ? limit : thumbnail_count_default);
5146 var numDesired = (allowUserLimit() ? thumbnail_count : numExpected);
5147
5148 if (!noResults && (numPosts !== numDesired || numPosts < numExpected))
5149 return true;
5150 }
5151 else if (mode === "popular" || mode === "pool" || mode === "favorite_group" || mode === "popular_view") {
5152 if (!noResults && numPosts !== limit)
5153 return true;
5154 }
5155 else if (mode === "comments") {
5156 if (numPosts !== limit)
5157 return true;
5158 }
5159
5160 return false;
5161 }
5162
5163 /* Endless Page functions */
5164 function endlessToggle(event) {
5165 // Toggle endless pages on and off.
5166 if (endless_default === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "favorites" && gLoc !== "favorite_group"))
5167 return;
5168
5169 // Change the default for the duration of the session if necessary.
5170 if (endless_session_toggle) {
5171 var onValue = (bbb.user.endless_default !== "off" ? bbb.user.endless_default : "on");
5172 var newDefault = (bbb.endless.enabled ? "off" : onValue);
5173
5174 endless_default = newDefault;
5175 sessionStorage.bbbSetItem("bbb_endless_default", newDefault);
5176 }
5177
5178 if (bbb.endless.enabled) {
5179 endlessDisable();
5180
5181 if (event && event.type !== "click")
5182 bbbNotice("Endless pages disabled.", 2);
5183 }
5184 else {
5185 endlessEnable();
5186
5187 if (event && event.type !== "click")
5188 bbbNotice("Endless pages enabled.", 2);
5189 }
5190 }
5191
5192 function endlessEnable() {
5193 // Turn on endless pages.
5194 if (endless_default === "disabled" || noXML())
5195 return;
5196
5197 bbb.endless.enabled = true;
5198 bbb.el.endlessEnableDiv.style.display = "none";
5199 bbb.el.endlessLoadDiv.style.display = "inline-block";
5200 bbb.el.endlessLink.style.fontWeight = "bold";
5201
5202 // Check on the next page status.
5203 endlessCheck();
5204
5205 // Add the listeners for detecting the amount of scroll left.
5206 window.addEventListener("scroll", endlessCheck, false);
5207 window.addEventListener("resize", endlessCheck, false);
5208 document.addEventListener("keyup", endlessCheck, false);
5209 document.addEventListener("click", endlessCheck, false);
5210 }
5211
5212 function endlessDisable() {
5213 // Turn off endless pages.
5214 bbb.endless.enabled = false;
5215 bbb.endless.append_page = false;
5216 bbb.el.endlessEnableDiv.style.display = "inline-block";
5217 bbb.el.endlessLoadDiv.style.display = "none";
5218 bbb.el.endlessLink.style.fontWeight = "normal";
5219
5220 // Remove the listeners for detecting the amount of scroll left.
5221 window.removeEventListener("scroll", endlessCheck, false);
5222 window.removeEventListener("resize", endlessCheck, false);
5223 document.removeEventListener("keyup", endlessCheck, false);
5224 document.removeEventListener("click", endlessCheck, false);
5225 }
5226
5227 function endlessInit() {
5228 // Set up and start endless pages.
5229 removeInheritedStorage("bbb_endless_default");
5230
5231 var paginator = getPaginator();
5232
5233 if (endless_default === "disabled" || !paginator || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "favorites" && gLoc !== "favorite_group"))
5234 return;
5235
5236 // Add the endless link to the menu.
5237 var listingItem = document.getElementById("subnav-listing") || document.getElementById("subnav-view-posts");
5238
5239 if (listingItem) {
5240 var menu = listingItem.parentNode;
5241 var listingItemSibling = listingItem.nextElementSibling;
5242
5243 var link = bbb.el.endlessLink = document.createElement("a");
5244 link.href = "#";
5245 link.innerHTML = "Endless";
5246 link.addEventListener("click", function(event) {
5247 if (event.button !== 0)
5248 return;
5249
5250 endlessToggle();
5251 event.preventDefault();
5252 }, false);
5253
5254 var item = document.createElement("li");
5255 item.style.textAlign = "center";
5256 item.style.display = "inline-block";
5257 item.appendChild(link);
5258
5259 if (listingItemSibling)
5260 menu.insertBefore(item, listingItemSibling);
5261 else
5262 menu.appendChild(item);
5263
5264 link.style.fontWeight = "bold";
5265 item.style.width = item.clientWidth + "px";
5266 link.style.fontWeight = "normal";
5267 }
5268
5269 var paginatorParent = paginator.parentNode;
5270
5271 // Set up the load more button.
5272 var buttonDiv = document.createElement("div");
5273 buttonDiv.id = "bbb-endless-button-div";
5274
5275 var loadButtonDiv = bbb.el.endlessLoadDiv = document.createElement("div");
5276 loadButtonDiv.id = "bbb-endless-load-div";
5277 buttonDiv.appendChild(loadButtonDiv);
5278
5279 var loadToggle = function() {
5280 // Continue page loading after hitting the pause interval.
5281 if (bbb.endless.paused !== true)
5282 return;
5283
5284 bbb.el.endlessLoadButton.style.display = "none";
5285 bbb.el.endlessLoadButton.blur();
5286 bbb.endless.paused = false;
5287 bbb.endless.append_page = true;
5288 endlessCheck();
5289 };
5290
5291 var loadButton = bbb.el.endlessLoadButton = document.createElement("a");
5292 loadButton.innerHTML = "Load More";
5293 loadButton.href = "#";
5294 loadButton.id = "bbb-endless-load-button";
5295 loadButton.style.display = "none";
5296 loadButton.addEventListener("click", function(event) {
5297 if (event.button !== 0)
5298 return;
5299
5300 loadToggle();
5301 event.preventDefault();
5302 }, false);
5303 loadButtonDiv.appendChild(loadButton);
5304
5305 // Set up the enable button.
5306 var enableButtonDiv = bbb.el.endlessEnableDiv = document.createElement("div");
5307 enableButtonDiv.id = "bbb-endless-enable-div";
5308 buttonDiv.appendChild(enableButtonDiv);
5309
5310 var enableButton = document.createElement("a");
5311 enableButton.innerHTML = "Endless";
5312 enableButton.href = "#";
5313 enableButton.id = "bbb-endless-enable-button";
5314 enableButton.addEventListener("click", function(event) {
5315 if (event.button !== 0)
5316 return;
5317
5318 enableButton.blur();
5319 endlessToggle();
5320 event.preventDefault();
5321 }, false);
5322 enableButtonDiv.appendChild(enableButton);
5323
5324 paginatorParent.insertBefore(buttonDiv, paginator);
5325
5326 // Create the hotkeys.
5327 createHotkey("69", endlessToggle); // E
5328 createHotkey("s69", loadToggle); // Shift + E
5329
5330 // Check the session default or original default value to see if endless pages should be enabled.
5331 var sessionDefault = sessionStorage.getItem("bbb_endless_default");
5332
5333 if (endless_session_toggle && sessionDefault)
5334 endless_default = sessionDefault;
5335
5336 if (endless_default !== "off")
5337 endlessEnable();
5338 else
5339 endlessDisable();
5340 }
5341
5342 function endlessObjectInit() {
5343 // Initialize the values for the first XML request. Runs separately from endlessInit since it requires the initial page being finalized.
5344 var posts = getPosts();
5345 var numPosts = posts.length;
5346
5347 // Prep the first paginator.
5348 bbb.endless.last_paginator = getPaginator();
5349
5350 // If we're already on the last page, don't continue.
5351 if (endlessLastPage())
5352 return;
5353
5354 // Note the posts that already exist.
5355 if (endless_remove_dup) {
5356 for (var i = 0; i < numPosts; i++) {
5357 var post = posts[i];
5358
5359 bbb.endless.posts[post.id] = post;
5360 }
5361 }
5362
5363 // Create a special "page" for filling out the first page.
5364 if (endless_fill) {
5365 var limit = getLimit() || thumbnail_count_default;
5366
5367 if (numPosts < limit) {
5368 var newPageObject = {
5369 page: document.createDocumentFragment(),
5370 page_num: [(getVar("page") || "1")],
5371 paginator: bbb.endless.last_paginator,
5372 ready: false
5373 };
5374
5375 bbb.endless.fill_first_page = true;
5376 bbb.endless.pages.push(newPageObject);
5377 }
5378 }
5379 }
5380
5381 function endlessCheck() {
5382 // Check whether the current document is ready for a page to be appended.
5383 if (!bbb.endless.enabled)
5384 return;
5385
5386 // Check whether endless pages needs to be paused.
5387 endlessPauseCheck();
5388
5389 // Stop if the check is delayed.
5390 if (bbb.timers.endlessDelay)
5391 return;
5392
5393 // Check whether a user is looking at the "posts tab" and not the "wiki tab" in the main search listing.
5394 var postsDiv = (gLoc === "search" ? document.getElementById("posts") : undefined);
5395 var postsVisible = (!postsDiv || postsDiv.style.display !== "none");
5396
5397 if (bbb.flags.thumbs_xml || bbb.flags.paginator_xml || !postsVisible) // Delay the check until the page is completely ready.
5398 endlessDelay(100);
5399 else {
5400 if (!bbb.endless.last_paginator)
5401 endlessObjectInit();
5402
5403 if (bbb.endless.append_page)
5404 endlessQueueCheck();
5405 else { // Check the amount of space left to scroll and attempt to add a page if we're far enough down.
5406 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
5407 var viewHeight = document.documentElement.clientHeight;
5408 var docHeight = document.documentElement.offsetHeight;
5409
5410 if (docHeight <= viewHeight + scrolled + endless_scroll_limit)
5411 bbb.endless.append_page = true;
5412
5413 endlessQueueCheck();
5414 }
5415 }
5416 }
5417
5418 function endlessQueueCheck() {
5419 // Check the page queue and append or request a page.
5420 if (!bbb.endless.enabled || bbb.endless.paused)
5421 return;
5422
5423 if (bbb.endless.append_page || bbb.endless.fill_first_page) {
5424 if (endlessPageReady())
5425 endlessAppendPage();
5426 else
5427 endlessRequestPage();
5428 }
5429 else if (endless_preload && !endlessPageReady())
5430 endlessRequestPage();
5431 }
5432
5433 function endlessRequestPage() {
5434 // Start an XML request for a new page.
5435 if (bbb.flags.endless_xml || endlessLastPage()) // Retrieve pages one at a time for as long as they exist.
5436 return;
5437
5438 searchPages("endless");
5439 }
5440
5441 function endlessQueuePage(newPage) {
5442 // Take some thumbnails from a page and work them into the queue.
5443 var limit = getLimit() || thumbnail_count_default;
5444 var pageNum = getVar("page", endlessNexURL());
5445 var paginator = bbb.endless.last_paginator = bbb.endless.new_paginator;
5446 var badPaginator = (paginator.textContent.indexOf("Go back") > -1); // Sometimes the paginator sends us to a page with no results.
5447 var lastPage = endlessLastPage() || badPaginator;
5448 var origPosts = getPosts(newPage);
5449 var i, il; // Loop variables.
5450
5451 bbb.endless.new_paginator = undefined;
5452
5453 // Remove duplicates.
5454 if (endless_remove_dup) {
5455 for (i = 0; i < origPosts.length; i++) {
5456 var post = origPosts[i];
5457 var postId = post.id;
5458
5459 if (bbb.endless.posts[postId])
5460 newPage.removeChild(post);
5461 else
5462 bbb.endless.posts[postId] = post;
5463 }
5464 }
5465
5466 // Fill up existing page objects with thumbnails.
5467 var fillPosts = getPosts(newPage);
5468 var leftoverPosts = fillPosts;
5469 var lastPageObject = bbb.endless.pages[bbb.endless.pages.length - 1];
5470
5471 if (endless_fill && lastPageObject && !lastPageObject.ready) {
5472 if (badPaginator) // Paginator isn't accurate. Ignore this page's number and paginator.
5473 lastPageObject.ready = true;
5474 else {
5475 var lastQueuePage = lastPageObject.page;
5476 var lastQueuePosts = getPosts(lastQueuePage);
5477 var fillLimit = (bbb.endless.fill_first_page ? limit - getPosts().length : limit - lastQueuePosts.length);
5478
5479 for (i = 0, il = fillPosts.length; i < il; i++) {
5480 lastQueuePage.appendChild(fillPosts[i]);
5481 fillLimit--;
5482
5483 if (fillLimit === 0) {
5484 lastPageObject.ready = true;
5485 break;
5486 }
5487 }
5488
5489 // If there are no more posts and pages, mark the last page as ready.
5490 leftoverPosts = getPosts(newPage);
5491
5492 if (!lastPageObject.ready && !leftoverPosts[0] && lastPage)
5493 lastPageObject.ready = true;
5494
5495 // Make sure the displayed paginator is always the one from the last retrieved page to have all of its thumbnails used so the user doesn't click to the next page and skip queued thumbnails that haven't been displayed yet.
5496 if (!leftoverPosts[0])
5497 lastPageObject.paginator = paginator;
5498
5499 lastPageObject.page_num.push(pageNum);
5500 }
5501 }
5502
5503 // Queue up a new page object.
5504 var numNewPosts = leftoverPosts.length;
5505
5506 if (numNewPosts > 0 || (!endless_fill && !badPaginator) || (endless_fill && !bbb.endless.pages[0] && !lastPage)) { // Queue the page if: 1) There are thumbnails. 2) It's normal mode and not a "no results" page. 3) It's fill mode and there is no object to work with for future pages.
5507 var newPageObject = {
5508 page: newPage,
5509 page_num: [pageNum],
5510 paginator: paginator,
5511 ready: (!endless_fill || numNewPosts === limit || lastPage)
5512 };
5513
5514 bbb.endless.pages.push(newPageObject);
5515
5516 if (bbb.endless.no_thumb_count < 10)
5517 bbb.endless.no_thumb_count = 0;
5518 }
5519 else if (!badPaginator)
5520 bbb.endless.no_thumb_count++;
5521
5522 // Get rid of the load more button for special circumstances where the paginator isn't accurate.
5523 if (lastPage && !bbb.endless.pages[0])
5524 bbb.el.endlessLoadButton.style.display = "none";
5525
5526 // Warn the user if this is a listing full of hidden posts.
5527 if (bbb.endless.no_thumb_count === 10) {
5528 bbbNotice("There have been no or very few thumbnails detected in the last 10 retrieved pages. Using endless pages with fill mode on this search could potentially be very slow or stall out completely. If you would like to continue, you may click the \"load more\" button near the bottom of the page.", -1);
5529 endlessPause();
5530 }
5531 else
5532 endlessQueueCheck();
5533 }
5534
5535 function endlessAppendPage() {
5536 // Prep the first queued page object and add it to the document.
5537 var firstPageObject = bbb.endless.pages.shift();
5538 var page = firstPageObject.page;
5539 var thumbContainer = getThumbContainer(gLoc);
5540 var before = getThumbSibling(gLoc);
5541
5542 // Prepare thumbnails.
5543 prepThumbnails(page);
5544
5545 // Page separators.
5546 var pageNum = firstPageObject.page_num;
5547 var numPageNum = pageNum.length;
5548 var firstNum = pageNum[0];
5549 var lastNum = (numPageNum > 1 ? pageNum[numPageNum - 1] : undefined);
5550
5551 if (endless_separator === "divider") {
5552 var divider = document.createElement("div");
5553 divider.className = "bbb-endless-divider";
5554
5555 var dividerLink = document.createElement("div");
5556 dividerLink.className = "bbb-endless-divider-link";
5557 divider.appendChild(dividerLink);
5558
5559 dividerLink.innerHTML = '<a href="' + updateURLQuery(location.href, {page: firstNum}) + '">Page ' + firstNum + '</a>' + (lastNum ? ' ~ <a href="' + updateURLQuery(location.href, {page: lastNum}) + '">Page ' + lastNum + '</a>' : '');
5560
5561 if (!bbb.endless.fill_first_page)
5562 page.insertBefore(divider, page.firstElementChild);
5563 else if (lastNum) // Only add the divider for filling the first page when there are actual posts added.
5564 thumbContainer.insertBefore(divider, (getPosts()[0] || before));
5565 }
5566 else if (endless_separator === "marker") {
5567 var markerContainer = document.createElement("article");
5568 markerContainer.className = "bbb-endless-marker-article";
5569
5570 var marker = document.createElement("div");
5571 marker.className = "bbb-endless-marker";
5572 markerContainer.appendChild(marker);
5573
5574 var markerLink = document.createElement("div");
5575 markerLink.className = "bbb-endless-marker-link";
5576 marker.appendChild(markerLink);
5577
5578 markerLink.innerHTML = '<a href="' + updateURLQuery(location.href, {page: firstNum}) + '">Page ' + firstNum + '</a>' + (lastNum ? '<br/>~<br/><a href="' + updateURLQuery(location.href, {page: lastNum}) + '">Page ' + lastNum + '</a>' : '');
5579
5580 if (!bbb.endless.fill_first_page)
5581 page.insertBefore(markerContainer, page.firstElementChild);
5582 else if (lastNum) // Only add the marker for filling the first page when there are actual posts added.
5583 thumbContainer.insertBefore(markerContainer, (getPosts()[0] || before));
5584 }
5585
5586 // Add the new page.
5587 if (!before)
5588 thumbContainer.appendChild(page);
5589 else
5590 thumbContainer.insertBefore(page, before);
5591
5592 // Replace the paginator.
5593 replacePaginator(firstPageObject.paginator);
5594
5595 if (!bbb.endless.fill_first_page)
5596 bbb.endless.append_page = false;
5597 else {
5598 bbb.endless.fill_first_page = false;
5599 endlessQueueCheck();
5600 }
5601
5602 if (quick_search.indexOf("remove") > -1 && bbb.quick_search.tags !== "")
5603 endlessDelay(1100);
5604
5605 endlessCheck();
5606 }
5607
5608 function endlessNexURL() {
5609 // Get the URL of the next new page.
5610 return getPaginatorNextURL(bbb.endless.last_paginator);
5611 }
5612
5613 function endlessPageReady() {
5614 // Check if the first queued page object is ready to be appended.
5615 var firstPageObject = bbb.endless.pages[0];
5616
5617 return (firstPageObject && firstPageObject.ready);
5618 }
5619
5620 function endlessLastPage() {
5621 // Check if there isn't a next page.
5622 return (!endlessNexURL() || noResultsPage());
5623 }
5624
5625 function endlessPauseCheck() {
5626 // Check if loading needs to be paused due to the interval or default.
5627 if (bbb.endless.append_page)
5628 return;
5629
5630 var numPages = document.getElementsByClassName("bbb-endless-page").length + 1;
5631
5632 if (numPages % endless_pause_interval === 0 || (endless_default === "paused" && numPages === 1))
5633 endlessPause();
5634 }
5635
5636 function endlessPause() {
5637 // Pause endless pages so that it can't add any more pages.
5638 if (bbb.endless.paused || (endlessLastPage() && !bbb.endless.pages[0]))
5639 return;
5640
5641 bbb.endless.paused = true;
5642 bbb.endless.append_page = false;
5643 bbb.el.endlessLoadButton.style.display = "inline-block";
5644 }
5645
5646 function endlessDelay(ms) {
5647 // Delay endless pages for the provided number of milliseconds.
5648 bbb.timers.endlessDelay = window.setTimeout( function() {
5649 bbb.timers.endlessDelay = 0;
5650
5651 endlessCheck();
5652 }, ms);
5653 }
5654
5655 /* Blacklist Functions */
5656 function blacklistInit() {
5657 // Reset the blacklist with the account settings when logged in or script settings when logged out/using the override.
5658 var blacklistTags = script_blacklisted_tags;
5659 var blacklistBox = document.getElementById("blacklist-box");
5660 var blacklistList = document.getElementById("blacklist-list");
5661 var enableLink = document.getElementById("re-enable-all-blacklists");
5662 var disableLink = document.getElementById("disable-all-blacklists");
5663 var blacklistedPosts = document.getElementsByClassName("blacklisted");
5664 var i, il; // Loop variables.
5665
5666 // Reset the list or create it as needed.
5667 if (blacklistBox && blacklistList) {
5668 blacklistBox.style.display = "none";
5669
5670 var childIndex = 0;
5671
5672 while (blacklistList.children[childIndex]) {
5673 var child = blacklistList.children[childIndex];
5674
5675 if (child.getElementsByTagName("a")[0] && child !== enableLink && child !== disableLink)
5676 blacklistList.removeChild(child);
5677 else
5678 childIndex++;
5679 }
5680 }
5681 else if (blacklist_add_bars) {
5682 var target, before; // If/else variables.
5683
5684 if (gLoc === "comment_search") {
5685 target = document.getElementById("a-index");
5686
5687 if (target)
5688 before = target.getElementsByClassName("comments-for-post")[0];
5689 }
5690 else if (gLoc === "comment") {
5691 target = document.getElementById("a-show");
5692
5693 if (target)
5694 before = target.getElementsByClassName("comments-for-post")[0];
5695 }
5696
5697 if (target && before && before.parentNode === target) {
5698 blacklistBox = document.createElement("div");
5699 blacklistBox.id = "blacklist-box";
5700 blacklistBox.className = "bbb-blacklist-box";
5701 blacklistBox.style.display = "none";
5702 blacklistBox.innerHTML = '<strong>Blacklisted: </strong> <ul id="blacklist-list"> <li id="disable-all-blacklists" style="display: inline;"><span class="link">Disable all</span></li> <li id="re-enable-all-blacklists" style="display: none;"><span class="link">Re-enable all</span></li> </ul>';
5703
5704 blacklistList = getId("blacklist-list", blacklistBox);
5705 enableLink = getId("re-enable-all-blacklists", blacklistBox);
5706 disableLink = getId("disable-all-blacklists", blacklistBox);
5707
5708 target.insertBefore(blacklistBox, before);
5709 }
5710 }
5711
5712 // Reset any blacklist info.
5713 if (bbb.blacklist.entries[0]) {
5714 delete bbb.blacklist;
5715 bbb.blacklist = {entries: [], match_list: {}, smart_view_target: undefined};
5716 }
5717
5718 // Reset any blacklisted thumbnails.
5719 var blacklistedPost = blacklistedPosts[0];
5720
5721 while (blacklistedPost) {
5722 blacklistedPost.bbbRemoveClass("blacklisted blacklisted-active");
5723 enablePostDDL(blacklistedPost);
5724 blacklistedPost = blacklistedPosts[0];
5725 }
5726
5727 // Check if there actually are any tags.
5728 if (!blacklistTags || !/[^\s,]/.test(blacklistTags))
5729 return;
5730
5731 // Preserve commas within nested/grouped tags.
5732 var groupsObject = replaceSearchGroups(blacklistTags);
5733 var groups = groupsObject.groups;
5734
5735 blacklistTags = groupsObject.search.replace(/,/g, "BBBCOMMASUB");
5736 blacklistTags = restoreSearchGroups(blacklistTags, groups);
5737 blacklistTags = blacklistTags.split("BBBCOMMASUB");
5738
5739 // Create the blacklist section.
5740 var cookies = getCookie();
5741 var blacklistDisabled = (cookies.dab === "1" && blacklistBox);
5742
5743 for (i = 0, il = blacklistTags.length; i < il; i++) {
5744 var blacklistTag = blacklistTags[i].bbbSpaceClean();
5745 var blacklistSearch = createSearch(blacklistTag);
5746
5747 if (blacklistSearch[0]) {
5748 var entryHash = blacklistTag.bbbHash();
5749 var entryDisabled = (blacklistDisabled || (blacklist_session_toggle && cookies["b" + entryHash] === "1"));
5750 var newEntry = {active: !entryDisabled, tags:blacklistTag, search:blacklistSearch, matches: [], index: i, hash: entryHash};
5751
5752 bbb.blacklist.entries.push(newEntry);
5753
5754 if (blacklistList) {
5755 var blacklistItem = document.createElement("li");
5756 blacklistItem.title = blacklistTag;
5757 blacklistItem.className = "bbb-blacklist-item-" + i;
5758 blacklistItem.style.display = "none";
5759
5760 var blacklistLink = document.createElement("a");
5761 blacklistLink.href = "#";
5762 blacklistLink.innerHTML = (blacklistTag.length < 19 ? blacklistTag + " " : blacklistTag.substring(0, 18).bbbSpaceClean() + "... ");
5763 blacklistLink.className = "bbb-blacklist-entry-" + i + (entryDisabled ? " blacklisted-active" : "");
5764 blacklistLink.bbbInfo("bbb-blacklist-entry", i);
5765 blacklistLink.addEventListener("click", blacklistEntryLinkToggle, false);
5766 blacklistItem.appendChild(blacklistLink);
5767
5768 var blacklistCount = document.createElement("span");
5769 blacklistCount.className = "count";
5770 blacklistCount.innerHTML = "0";
5771 blacklistItem.appendChild(blacklistCount);
5772
5773 blacklistList.appendChild(blacklistItem);
5774 }
5775 }
5776 }
5777
5778 // Replace the disable/enable all blacklist links with our own.
5779 if (enableLink && disableLink) {
5780 var newEnableLink = bbb.el.blacklistEnableLink = enableLink.cloneNode(true);
5781 var newDisableLink = bbb.el.blacklistDisableLink = disableLink.cloneNode(true);
5782
5783 newEnableLink.addEventListener("click", blacklistLinkToggle, false);
5784 newDisableLink.addEventListener("click", blacklistLinkToggle, false);
5785
5786 if (blacklistDisabled) {
5787 newEnableLink.style.display = "inline";
5788 newDisableLink.style.display = "none";
5789 }
5790 else {
5791 newEnableLink.style.display = "none";
5792 newDisableLink.style.display = "inline";
5793 }
5794
5795 enableLink.parentNode.replaceChild(newEnableLink, enableLink);
5796 disableLink.parentNode.replaceChild(newDisableLink, disableLink);
5797 }
5798
5799 // Test all posts on the page for a match and set up the initial blacklist.
5800 blacklistUpdate();
5801 }
5802
5803 function blacklistLinkToggle(event) {
5804 // Event listener function for permanently toggling the entire blacklist.
5805 if (event.button !== 0)
5806 return;
5807
5808 var blacklistDisabled = (getCookie().dab === "1");
5809 var entries = bbb.blacklist.entries;
5810
5811 if (blacklistDisabled) {
5812 bbb.el.blacklistEnableLink.style.display = "none";
5813 bbb.el.blacklistDisableLink.style.display = "inline";
5814 createCookie("dab", 0, 365);
5815 }
5816 else {
5817 bbb.el.blacklistEnableLink.style.display = "inline";
5818 bbb.el.blacklistDisableLink.style.display = "none";
5819 createCookie("dab", 1, 365);
5820 }
5821
5822 for (var i = 0, il = entries.length; i < il; i++) {
5823 var entry = entries[i];
5824
5825 if (blacklistDisabled) {
5826 if (!entry.active)
5827 blacklistEntryToggle(i);
5828 }
5829 else {
5830 if (entry.active)
5831 blacklistEntryToggle(i);
5832
5833 if (blacklist_session_toggle)
5834 createCookie("b" + entry.hash, 0, -1);
5835 }
5836 }
5837
5838 event.preventDefault();
5839 }
5840
5841 function blacklistEntryLinkToggle(event) {
5842 // Event listener function for blacklist entry toggle links.
5843 if (event.button !== 0)
5844 return;
5845
5846 var entryNumber = Number(event.target.bbbInfo("bbb-blacklist-entry"));
5847
5848 blacklistEntryToggle(entryNumber);
5849
5850 event.preventDefault();
5851 }
5852
5853 function blacklistEntryToggle(entryIndex) {
5854 // Toggle a blacklist entry and adjust all of its related elements.
5855 var entry = bbb.blacklist.entries[entryIndex];
5856 var matches = entry.matches;
5857 var links = document.getElementsByClassName("bbb-blacklist-entry-" + entryIndex);
5858 var blacklistDisabled = (getCookie().dab === "1");
5859 var i, il, j, jl, id, els, matchList; // Loop variables.
5860
5861 if (entry.active) {
5862 entry.active = false;
5863
5864 if (blacklist_session_toggle && !blacklistDisabled)
5865 createCookie("b" + entry.hash, 1);
5866
5867 for (i = 0, il = links.length; i < il; i++)
5868 links[i].bbbAddClass("blacklisted-active");
5869
5870 for (i = 0, il = matches.length; i < il; i++) {
5871 id = matches[i];
5872 matchList = bbb.blacklist.match_list[id];
5873
5874 matchList.count--;
5875
5876 if (!matchList.count && matchList.override !== false) {
5877 if (id === "image-container")
5878 blacklistShowPost(document.getElementById("image-container"));
5879 else {
5880 els = document.getElementsByClassName(id);
5881
5882 for (j = 0, jl = els.length; j < jl; j++)
5883 blacklistShowPost(els[j]);
5884 }
5885 }
5886 }
5887 }
5888 else {
5889 entry.active = true;
5890
5891 if (blacklist_session_toggle)
5892 createCookie("b" + entry.hash, 0, -1);
5893
5894 for (i = 0, il = links.length; i < il; i++)
5895 links[i].bbbRemoveClass("blacklisted-active");
5896
5897 for (i = 0, il = matches.length; i < il; i++) {
5898 id = matches[i];
5899 matchList = bbb.blacklist.match_list[id];
5900
5901 matchList.count++;
5902
5903 if (matchList.override !== true) {
5904 if (id === "image-container")
5905 blacklistHidePost(document.getElementById("image-container"));
5906 else {
5907 els = document.getElementsByClassName(id);
5908
5909 for (j = 0, jl = els.length; j < jl; j++)
5910 blacklistHidePost(els[j]);
5911 }
5912 }
5913 }
5914 }
5915 }
5916
5917 function blacklistUpdate(target) {
5918 // Update the blacklists without resetting everything.
5919 if (!bbb.blacklist.entries[0])
5920 return;
5921
5922 // Retrieve the necessary elements from the target element or current document.
5923 var blacklistBox = getId("blacklist-box", target) || document.getElementById("blacklist-box");
5924 var blacklistList = getId("blacklist-list", target) || document.getElementById("blacklist-list");
5925 var imgContainer = getId("image-container", target);
5926 var posts = getPosts(target);
5927
5928 var i, il; // Loop variables.
5929
5930 // Test the image for a match when viewing a post.
5931 if (imgContainer) {
5932 var imgId = imgContainer.bbbInfo("id");
5933
5934 if (!blacklistSmartViewCheck(imgId))
5935 blacklistTest(imgContainer);
5936 }
5937
5938 // Search the posts for matches.
5939 for (i = 0, il = posts.length; i < il; i++)
5940 blacklistTest(posts[i]);
5941
5942 // Update the blacklist sidebar section match counts and display any blacklist items that have a match.
5943 if (blacklistBox && blacklistList) {
5944 for (i = 0, il = bbb.blacklist.entries.length; i < il; i++) {
5945 var entryLength = bbb.blacklist.entries[i].matches.length;
5946 var item = blacklistList.getElementsByClassName("bbb-blacklist-item-" + i)[0];
5947
5948 if (entryLength) {
5949 blacklistBox.style.display = "block";
5950 item.style.display = "";
5951 item.getElementsByClassName("count")[0].innerHTML = entryLength;
5952 }
5953 }
5954 }
5955 }
5956
5957 function blacklistTest(el) {
5958 // Test a post/image for a blacklist match and use its ID to store its info.
5959 var id = el.id;
5960 var matchList = bbb.blacklist.match_list[id];
5961
5962 // Test posts that haven't been tested yet.
5963 if (typeof(matchList) === "undefined") {
5964 matchList = bbb.blacklist.match_list[id] = {count: undefined, matches: [], override: undefined};
5965
5966 if (!blacklist_ignore_fav || el.bbbInfo("is-favorited") !== "true") {
5967 for (var i = 0, il = bbb.blacklist.entries.length; i < il; i++) {
5968 var entry = bbb.blacklist.entries[i];
5969
5970 if (thumbSearchMatch(el, entry.search)) {
5971 if (entry.active)
5972 matchList.count = ++matchList.count || 1;
5973 else
5974 matchList.count = matchList.count || 0;
5975
5976 matchList.matches.push(entry);
5977 entry.matches.push(id);
5978 }
5979 }
5980 }
5981
5982 if (matchList.count === undefined) // No match.
5983 matchList.count = false;
5984 }
5985
5986 // Check the saved blacklist info for the post and change the thumbnail as needed.
5987 if (matchList.count !== false && !el.bbbHasClass("blacklisted")) {
5988 el.bbbAddClass("blacklisted");
5989
5990 if (matchList.count > 0 && matchList.override !== true)
5991 blacklistHidePost(el);
5992
5993 if (el.id !== "image-container") {
5994 if (blacklist_thumb_controls)
5995 blacklistPostControl(el, matchList);
5996
5997 if (blacklist_smart_view)
5998 blacklistSmartView(el);
5999 }
6000 }
6001 }
6002
6003 function blacklistPostControl(el, matchList) {
6004 // Add the blacklist post controls to a thumbnail.
6005 var target = el.getElementsByClassName("preview")[0] || el;
6006 var id = el.id;
6007 var tip = bbb.el.blacklistTip;
6008
6009 if (!tip) { // Create the tip if it doesn't exist.
6010 tip = bbb.el.blacklistTip = document.createElement("div");
6011 tip.id = "bbb-blacklist-tip";
6012 document.body.appendChild(tip);
6013 }
6014
6015 if (target) {
6016 // Set up the tip events listeners for hiding and displaying it.
6017 target.addEventListener("click", function(event) {
6018 if (event.button !== 0 || event.ctrlKey || event.shiftKey || event.altKey)
6019 return;
6020
6021 var target = event.target;
6022 var blacklistTip = bbb.el.blacklistTip;
6023 var i, il; // Loop variables.
6024
6025 if (!el.bbbHasClass("blacklisted-active") || (target.tagName === "A" && !target.bbbHasClass("bbb-thumb-link"))) // If the thumb isn't currently hidden or a link that isn't the thumb link is clicked, allow the link click.
6026 return;
6027
6028 if (blacklistTip.style.display !== "block") {
6029 var matchEntries = matchList.matches;
6030 var tipContent = document.createDocumentFragment();
6031
6032 var header = document.createElement("b");
6033 header.innerHTML = "Blacklist Matches";
6034 tipContent.appendChild(header);
6035
6036 var list = document.createElement("ul");
6037 tipContent.appendChild(list);
6038
6039 for (i = 0, il = matchEntries.length; i < il; i++) {
6040 var matchEntry = matchEntries[i];
6041 var entryIndex = matchEntry.index;
6042 var blacklistTag = matchEntry.tags;
6043
6044 var blacklistItem = document.createElement("li");
6045 blacklistItem.title = blacklistTag;
6046
6047 var blacklistLink = document.createElement("a");
6048 blacklistLink.href = "#";
6049 blacklistLink.className = "bbb-blacklist-entry-" + entryIndex + (matchEntry.active ? "" : " blacklisted-active");
6050 blacklistLink.bbbInfo("bbb-blacklist-entry", entryIndex);
6051 blacklistLink.innerHTML = (blacklistTag.length < 51 ? blacklistTag + " " : blacklistTag.substring(0, 50).bbbSpaceClean() + "...");
6052 blacklistLink.addEventListener("click", blacklistEntryLinkToggle, false);
6053 blacklistItem.appendChild(blacklistLink);
6054
6055 list.appendChild(blacklistItem);
6056 }
6057
6058 var viewLinkDiv = document.createElement("div");
6059 viewLinkDiv.style.marginTop = "1em";
6060 viewLinkDiv.style.textAlign = "center";
6061 viewLinkDiv.innerHTML = '<a class="bbb-post-link" id="bbb-blacklist-view-link" href="/posts/' + id.match(/\d+/)[0] + '">View post</a>';
6062 tipContent.appendChild(viewLinkDiv);
6063
6064 if (blacklist_smart_view) {
6065 var viewLink = getId("bbb-blacklist-view-link", viewLinkDiv);
6066
6067 if (viewLink) {
6068 viewLink.addEventListener("click", function(event) {
6069 if (event.button === 0)
6070 blacklistSmartViewUpdate(el);
6071 }, false);
6072 }
6073 }
6074
6075 blacklistShowTip(event, tipContent);
6076 }
6077 else {
6078 var els = document.getElementsByClassName(id);
6079
6080 for (i = 0, il = els.length; i < il; i++)
6081 blacklistShowPost(els[i]);
6082
6083 blacklistHideTip();
6084 bbb.blacklist.match_list[id].override = true;
6085 }
6086
6087 event.preventDefault();
6088 event.stopPropagation();
6089 }, true);
6090 target.addEventListener("mouseleave", function() { bbb.timers.blacklistTip = window.setTimeout(blacklistHideTip, 100); }, false);
6091 tip.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.blacklistTip); }, false);
6092 tip.addEventListener("mouseleave", blacklistHideTip, false);
6093
6094 // Add the hide button.
6095 var hide = document.createElement("span");
6096 hide.className = "bbb-close-circle";
6097 hide.addEventListener("click", function(event) {
6098 if (event.button !== 0)
6099 return;
6100
6101 var els = document.getElementsByClassName(id);
6102
6103 for (var i = 0, il = els.length; i < il; i++)
6104 blacklistHidePost(els[i]);
6105
6106 bbb.blacklist.match_list[id].override = false;
6107 }, false);
6108 target.appendChild(hide);
6109 }
6110 }
6111
6112 function blacklistShowTip(event, content) {
6113 // Display the blacklist control tip.
6114 var x = event.pageX;
6115 var y = event.pageY;
6116 var tip = bbb.el.blacklistTip;
6117
6118 formatTip(event, tip, content, x, y);
6119 }
6120
6121 function blacklistHideTip() {
6122 // Reset the blacklist control tip to hidden.
6123 var tip = bbb.el.blacklistTip;
6124
6125 if (tip)
6126 tip.removeAttribute("style");
6127 }
6128
6129 function blacklistSmartView(el) {
6130 // Set up the smart view event listeners.
6131 var img = el.getElementsByTagName("img")[0];
6132 var link = (img ? img.bbbParent("A", 3) : undefined);
6133
6134 if (!link)
6135 return;
6136
6137 // Normal left click support.
6138 link.addEventListener("click", function(event) {
6139 if (event.button === 0)
6140 blacklistSmartViewUpdate(el);
6141 }, false);
6142
6143 // Right and middle button click support.
6144 link.addEventListener("mousedown", function(event) {
6145 if (event.button === 1)
6146 bbb.blacklist.smart_view_target = link;
6147 }, false);
6148 link.addEventListener("mouseup", function(event) {
6149 if (event.button === 1 && bbb.blacklist.smart_view_target === link)
6150 blacklistSmartViewUpdate(el);
6151 else if (event.button === 2)
6152 blacklistSmartViewUpdate(el);
6153 }, false);
6154 }
6155
6156 function blacklistSmartViewUpdate(el) {
6157 // Update the blacklisted thumbnail info in the smart view object.
6158 var time = new Date().getTime();
6159 var id = el.bbbInfo("id");
6160 var smartView = localStorage.getItem("bbb_smart_view");
6161
6162 if (smartView === null) // Initialize the object if it doesn't exist.
6163 smartView = {last: time};
6164 else {
6165 smartView = parseJson(smartView, {last: time});
6166
6167 if (time - smartView.last > 60000) // Reset the object if it hasn't been changed within a minute.
6168 smartView = {last: time};
6169 else
6170 smartView.last = time; // Adjust the object.
6171 }
6172
6173 if (!el.bbbHasClass("blacklisted-active"))
6174 smartView[id] = time;
6175 else
6176 delete smartView[id];
6177
6178 localStorage.bbbSetItem("bbb_smart_view", JSON.stringify(smartView));
6179 }
6180
6181 function blacklistSmartViewCheck(id) {
6182 // Check whether to display the post during the blacklist init.
6183 var smartView = localStorage.getItem("bbb_smart_view");
6184
6185 if (!blacklist_smart_view || smartView === null)
6186 return false;
6187 else {
6188 var time = new Date().getTime();
6189
6190 smartView = parseJson(smartView, {last: time});
6191
6192 if (time - smartView.last > 60000) { // Delete the ids if the object hasn't been changed within a minute.
6193 localStorage.removeItem("bbb_smart_view");
6194 return false;
6195 }
6196 else if (!smartView[id]) // Return false if the id isn't found.
6197 return false;
6198 else if (time - smartView[id] > 60000) // Return false if the click is over a minute ago.
6199 return false;
6200 }
6201
6202 return true;
6203 }
6204
6205 function blacklistHidePost(post) {
6206 // Hide blacklisted post content and adjust related content.
6207 if (blacklist_video_playback.indexOf("pause") > -1 && gLoc === "post") {
6208 var postEl = getPostContent(post).el;
6209
6210 if (postEl && postEl.tagName === "VIDEO")
6211 postEl.pause();
6212 }
6213
6214 post.bbbAddClass("blacklisted-active");
6215 disablePostDDL(post);
6216 }
6217
6218 function blacklistShowPost(post) {
6219 // Reveal blacklisted post content and adjust related content.
6220 if (blacklist_video_playback.indexOf("resume") > -1 && gLoc === "post") {
6221 var postEl = getPostContent(post).el;
6222
6223 if (postEl && postEl.tagName === "VIDEO" && ((video_autoplay === "on" || (!bbb.post.info.has_sound && video_autoplay === "nosound")) || postEl.played.length))
6224 postEl.play();
6225 }
6226
6227 post.bbbRemoveClass("blacklisted-active");
6228 enablePostDDL(post);
6229 }
6230
6231 /* Other functions */
6232 function modifyDanbScript() {
6233 // Modify some Danbooru functions so that they don't run unnecessarily.
6234 var loadNotes = Danbooru.Note.load_all;
6235
6236 Danbooru.Note.load_all = function(allow) {
6237 if (allow === "bbb")
6238 loadNotes();
6239 };
6240
6241 Danbooru.Blacklist.initialize_all = function() {
6242 return;
6243 };
6244 }
6245
6246 function modifyPage() {
6247 // Determine what function may be needed to fix/update original content.
6248 checkStateCache();
6249
6250 if (noXML())
6251 return;
6252
6253 var allowAPI = useAPI();
6254 var stateCache = (history.state || {}).bbb_posts_cache;
6255
6256 if (gLoc === "post")
6257 delayMe(parsePost); // Delay is needed to force the script to pause and allow Danbooru to do whatever. It essentially mimics the async nature of the API call.
6258 else if (gLoc === "comment_search" || gLoc === "comment")
6259 delayMe(fixCommentSearch);
6260 else if (stateCache) // Use a cached set of thumbnails.
6261 delayMe(function() { parseListing(formatInfoArray(stateCache.posts)); });
6262 else if (allowAPI && potentialHiddenPosts(gLoc)) // API only features.
6263 searchJSON(gLoc);
6264 else if (!allowAPI && allowUserLimit()) // Alternate mode for features.
6265 searchPages(gLoc);
6266
6267 // Cache any necessary info before leaving the page.
6268 window.addEventListener("beforeunload", saveStateCache);
6269
6270 // Flag if the page is being unloaded in order to differentiate it from other potential XML interruptions.
6271 window.addEventListener("beforeunload", function(){ bbb.flags.unloading = true; });
6272 }
6273
6274 function formatInfo(postInfo) {
6275 // Add information to/alter information in the post object.
6276 if (!postInfo)
6277 return undefined;
6278
6279 // Hidden post fixes.
6280 postInfo.md5 = postInfo.md5 || "";
6281 postInfo.file_ext = postInfo.file_ext || "";
6282 postInfo.preview_file_url = postInfo.preview_file_url || "";
6283 postInfo.large_file_url = postInfo.large_file_url || "";
6284 postInfo.file_url = postInfo.file_url || "";
6285
6286 // Potential null value fixes.
6287 postInfo.approver_id = postInfo.approver_id || "";
6288 postInfo.parent_id = postInfo.parent_id || "";
6289 postInfo.pixiv_id = postInfo.pixiv_id || "";
6290
6291 // Figure out sample image dimensions and ratio.
6292 postInfo.large_ratio = (postInfo.image_width > 850 ? 850 / postInfo.image_width : 1);
6293 postInfo.large_height = Math.round(postInfo.image_height * postInfo.large_ratio);
6294 postInfo.large_width = Math.round(postInfo.image_width * postInfo.large_ratio);
6295
6296 // Missing API/data fixes.
6297 postInfo.has_sound = /(?:^|\s)(?:video|flash)_with_sound(?:$|\s)/.test(postInfo.tag_string);
6298 postInfo.flags = postFlags(postInfo);
6299 postInfo.normalized_source = postInfo.normalized_source || normalizedSource(postInfo);
6300 postInfo.keeper_data = postInfo.keeper_data || {uid: postInfo.uploader_id};
6301 postInfo.uploader_name = (isModLevel() ? postInfo.uploader_name : "");
6302
6303 // Custom BBB properties.
6304 postInfo.tag_string_desc = tagStringDesc(postInfo);
6305 postInfo.file_url_desc = postFileUrlDesc(postInfo);
6306 postInfo.thumb_class = postClasses(postInfo);
6307 postFileSrcs(postInfo);
6308
6309 return postInfo;
6310 }
6311
6312 function formatInfoArray(array) {
6313 // Add information to/alter information to post objects in an array.
6314 var newArray = [];
6315
6316 for (var i = 0, il = array.length; i < il; i++)
6317 newArray.push(formatInfo(array[i]));
6318
6319 return newArray;
6320 }
6321
6322 function unformatInfo(postInfo) {
6323 // Remove extra BBB properties from post info.
6324 delete postInfo.large_ratio;
6325 delete postInfo.large_height;
6326 delete postInfo.large_width;
6327 delete postInfo.has_sound;
6328 delete postInfo.flags;
6329 delete postInfo.thumb_class;
6330 delete postInfo.crop_img_src;
6331 delete postInfo.preview_img_src;
6332 delete postInfo.file_img_src;
6333 delete postInfo.large_file_img_src;
6334 delete postInfo.tag_string_desc;
6335 delete postInfo.normalized_source;
6336
6337 return postInfo;
6338 }
6339
6340 function postFlags(postInfo) {
6341 // Figure out the flags for a post using its post information.
6342 var flags = "";
6343
6344 if (postInfo.is_deleted)
6345 flags += " deleted";
6346 if (postInfo.is_pending)
6347 flags += " pending";
6348 if (postInfo.is_banned)
6349 flags += " banned";
6350 if (postInfo.is_flagged)
6351 flags += " flagged";
6352
6353 return flags.bbbSpaceClean();
6354 }
6355
6356 function postClasses(postInfo) {
6357 // Figure out the thumbnail classes for a post using its post information.
6358 var thumbClass = postInfo.thumb_class || "";
6359
6360 // Skip if the classes already exist.
6361 if (thumbClass.indexOf("post-status-") > -1)
6362 return thumbClass;
6363
6364 if (postInfo.is_deleted)
6365 thumbClass += " post-status-deleted";
6366 if (postInfo.is_pending)
6367 thumbClass += " post-status-pending";
6368 if (postInfo.is_flagged)
6369 thumbClass += " post-status-flagged";
6370 if (postInfo.has_children && (postInfo.has_active_children || show_deleted))
6371 thumbClass += " post-status-has-children";
6372 if (postInfo.parent_id)
6373 thumbClass += " post-status-has-parent";
6374
6375 return thumbClass;
6376 }
6377
6378 function tagStringList(string) {
6379 // Create a Danbooru style tag string list out of the first 5 tags for a category.
6380 var array = string.bbbSpaceClean().split(" ");
6381 var length = array.length;
6382 var result;
6383
6384 if (length > 2) {
6385 if (length > 5) {
6386 array = array.slice(0,5);
6387 array.push("and others");
6388 }
6389 else
6390 array[length - 1] = "and " + array[length - 1];
6391
6392 result = array.join(", ");
6393 }
6394 else if (length === 2)
6395 result = array.join(" and ");
6396 else
6397 result = array[0];
6398
6399 return result;
6400 }
6401
6402 function tagStringDesc(postInfo) {
6403 // Create a Danbooru style post description.
6404 var downloadName = (getMeta("og:title") || "").replace(" - Danbooru", "");
6405 var desc;
6406
6407 if (gLoc === "post" && downloadName)
6408 desc = downloadName;
6409 else if (typeof(postInfo.tag_string_artist) === "string") {
6410 var artists = tagStringList(postInfo.tag_string_artist.replace(/(?:^|\s)banned_artist(?:$|\s)/, " "));
6411 var characters = tagStringList(postInfo.tag_string_character.replace(/_\([^)]+\)($|\s)/g, "$1"));
6412 var copyrights = tagStringList(postInfo.tag_string_copyright);
6413
6414 if (characters !== "" && copyrights !== "")
6415 copyrights = " (" + copyrights + ")";
6416
6417 if (artists !== "")
6418 artists = " drawn by " + artists;
6419
6420 desc = (characters + copyrights + artists).bbbSpaceClean() || "#" + postInfo.id;
6421 }
6422 else
6423 desc = "";
6424
6425 return desc;
6426 }
6427
6428 function tagStringFileUrlDesc(postInfo) {
6429 // Create a Danbooru style post file/download description.
6430 var fileDesc = (postInfo.tag_string_desc ? "__" + postInfo.tag_string_desc.replace(/[^0-9a-z]+/g, "_").replace(/^_+|_+$/g, "") + "__" : "");
6431
6432 return fileDesc;
6433 }
6434
6435 function postFileUrlDesc(postInfo) {
6436 // Create/return the file URL desc property.
6437 if (gLoc !== "post") // Danbooru doesn't provide this information for thumbnail listings.
6438 return "";
6439 else if (typeof(postInfo.file_url_desc) === "string")
6440 return postInfo.file_url_desc;
6441 else if (postInfo.file_url.indexOf("__") > -1)
6442 return "__" + postInfo.file_url.split("__")[1] + "__";
6443 else
6444 return tagStringFileUrlDesc(postInfo);
6445 }
6446
6447 function postFileSrcs(postInfo) {
6448 // Add the BBB file URL properties to post info.
6449 postInfo.preview_img_src = postInfo.preview_file_url || bbbHiddenImg;
6450
6451 if (postInfo.md5) {
6452 var pathChars = postInfo.md5.match(/^(..)(..)/);
6453
6454 postInfo.crop_img_src = "https://raikou3.donmai.us/crop/" + pathChars[1] + "/" + pathChars[2] + "/" + postInfo.md5 + ".jpg";
6455 }
6456 else
6457 postInfo.crop_img_src = bbbHiddenImg;
6458
6459 if (disable_tagged_filenames && postInfo.file_url.indexOf("__") > -1) {
6460 postInfo.file_img_src = postInfo.file_url.replace(postInfo.file_url_desc, "");
6461 postInfo.large_file_img_src = postInfo.large_file_url.replace(postInfo.file_url_desc, "");
6462 }
6463 else if (!disable_tagged_filenames && postInfo.file_url.indexOf("__") < 0) {
6464 postInfo.file_img_src = postInfo.file_url.replace(/\/(?=[\w\-]+\.\w+$)/, "/" + postInfo.file_url_desc);
6465 postInfo.large_file_img_src = postInfo.large_file_url.replace(/\/(?=[\w\-]+\.\w+$)/, "/" + postInfo.file_url_desc);
6466 }
6467 else {
6468 postInfo.file_img_src = postInfo.file_url;
6469 postInfo.large_file_img_src = postInfo.large_file_url;
6470 }
6471 }
6472
6473 function normalizedSource(postInfo) {
6474 // Produce a normalized source from a post's source information.
6475 var source = postInfo.source;
6476 var urlReg, url, id, artist, title; // If/else variables.
6477
6478 if (!!(urlReg = source.match(/^https?:\/\/img\d+\.pixiv\.net\/img\/[^\/]+\/(\d+)/i) || source.match(/^https?:\/\/i\d\.pixiv\.net\/img\d+\/img\/[^\/]+\/(\d+)/i)))
6479 url = "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=" + urlReg[1];
6480 else if (!!(urlReg = source.match(/^https?:\/\/(?:i\d+\.pixiv\.net|i\.pximg\.net)\/img-(?:master|original)\/img\/(?:\d+\/)+(\d+)_p/i) || source.match(/^https?:\/\/(?:i\d+\.pixiv\.net|i\.pximg\.net)\/c\/\d+x\d+\/img-master\/img\/(?:\d+\/)+(\d+)_p/i) || source.match(/^https?:\/\/(?:i\d+\.pixiv\.net|i\.pximg\.net)\/img-zip-ugoira\/img\/(?:\d+\/)+(\d+)_ugoira\d+x\d+\.zip/i)))
6481 url = "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=" + urlReg[1];
6482 else if (!!(urlReg = source.match(/^https?:\/\/lohas\.nicoseiga\.jp\/priv\/(\d+)\?e=\d+&h=[a-f0-9]+/i) || source.match(/^https?:\/\/lohas\.nicoseiga\.jp\/priv\/[a-f0-9]+\/\d+\/(\d+)/i)))
6483 url = "https://seiga.nicovideo.jp/seiga/im" + urlReg[1];
6484 else if (!!(urlReg = source.match(/^https?:\/\/(?:d3j5vwomefv46c|dn3pm25xmtlyu)\.cloudfront\.net\/photos\/large\/(\d+)\./i))) {
6485 id = parseInt(urlReg[1]).toString(36);
6486 url = "https://twitpic.com/" + id;
6487 }
6488 else if (!!(urlReg = source.match(/^https?:\/\/(?:(?:fc|th|pre|orig|img|prnt)\d{2}|origin-orig)\.deviantart\.net\/.+\/([a-z0-9_]+)_by_([a-z0-9_]+)-d([a-z0-9]+)\./i))) {
6489 title = urlReg[1].replace(/[^A-Za-z0-9]/g, " ").bbbSpaceClean().replace(/[ ]/g, "-");
6490 artist = urlReg[2].replace(/_/g, "-");
6491 id = parseInt(urlReg[3], 36);
6492 url = "https://www.deviantart.com/" + artist + "/art/" + title + "-" + id;
6493 }
6494 else if (!!(urlReg = source.match(/^https?:\/\/(?:fc|th|pre|orig|img|prnt)\d{2}\.deviantart\.net\/.+\/[a-f0-9]{32}-d([a-z0-9]+)\./i))) {
6495 id = parseInt(urlReg[1], 36);
6496 url = "https://deviantart.com/deviation/" + id;
6497 }
6498 else if (!!(urlReg = source.match(/^http:\/\/www\.karabako\.net\/images(?:ub)?\/karabako_(\d+)(?:_\d+)?\./i)))
6499 url = "http://www.karabako.net/post/view/" + urlReg[1];
6500 else if (!!(urlReg = source.match(/^http:\/\/p\.twpl\.jp\/show\/(?:large|orig)\/([a-z0-9]+)/i)))
6501 url = "http://p.twipple.jp/" + urlReg[1];
6502 else if (!!(urlReg = source.match(/^https?:\/\/pictures\.hentai-foundry\.com\/\/?[^\/]\/([^\/]+)\/(\d+)/i)))
6503 url = "https://www.hentai-foundry.com/pictures/user/" + urlReg[1] + "/" + urlReg[2];
6504 else if (!!(urlReg = source.match(/^http:\/\/blog(?:(?:-imgs-)?\d*(?:-origin)?)?\.fc2\.com\/(?:(?:[^\/]\/){3}|(?:[^\/]\/))([^\/]+)\/(?:file\/)?([^\.]+\.[^\?]+)/i)))
6505 url = "http://" + urlReg[1] + ".blog.fc2.com/img/" + urlReg[2] + "/";
6506 else if (!!(urlReg = source.match(/^http:\/\/diary(\d?)\.fc2\.com\/user\/([^\/]+)\/img\/(\d+)_(\d+)\/(\d+)\./i)))
6507 url = "http://diary" + urlReg[1] + ".fc2.com/cgi-sys/ed.cgi/" + urlReg[2] + "?Y=" + urlReg[3] + "&M=" + urlReg[4] + "&D=" + urlReg[5];
6508 else if (!!(urlReg = source.match(/^https?:\/\/(?:fbcdn-)?s(?:content|photos)-[^\/]+\.(?:fbcdn|akamaihd)\.net\/hphotos-.+\/\d+_(\d+)_(?:\d+_){1,3}[no]\./i)))
6509 url = "https://www.facebook.com/photo.php?fbid=" + urlReg[1];
6510 else if (!!(urlReg = source.match(/^https?:\/\/c(?:s|han|[1-4])\.sankakucomplex\.com\/data(?:\/sample)?\/(?:[a-f0-9]{2}\/){2}(?:sample-|preview)?([a-f0-9]{32})/i)))
6511 url = "https://chan.sankakucomplex.com/en/post/show?md5=" + urlReg[1];
6512 else if (!!(urlReg = source.match(/^http:\/\/s(?:tatic|[1-4])\.zerochan\.net\/.+(?:\.|\/)(\d+)\.(?:jpe?g?)$/i)))
6513 url = "https://www.zerochan.net/" + urlReg[1] + "#full";
6514 else if (!!(urlReg = source.match(/^http:\/\/static[1-6]?\.minitokyo\.net\/(?:downloads|view)\/(?:\d{2}\/){2}(\d+)/i)))
6515 url = "http://gallery.minitokyo.net/download/" + urlReg[1];
6516 else if (postInfo.md5 && !!(urlReg = source.match(/^https?:\/\/(?:\w+\.)?gelbooru\.com\/\/?(?:images|samples)\/(?:\d+|[a-f0-9]{2}\/[a-f0-9]{2})\/(?:sample_)?(?:[a-f0-9]{32}|[a-f0-9]{40})\./i)))
6517 url = "https://gelbooru.com/index.php?page=post&s=list&md5=" + postInfo.md5;
6518 else if (!!(urlReg = source.match(/^https?:\/\/(?:slot\d*\.)?im(?:g|ages)\d*\.wikia\.(?:nocookie\.net|com)\/(?:_{2}cb\d{14}\/)?([^\/]+)(?:\/[a-z]{2})?\/images\/(?:(?:thumb|archive)?\/)?[a-f0-9]\/[a-f0-9]{2}\/(?:\d{14}(?:!|%21))?([^\/]+)/i)))
6519 url = "http://" + urlReg[1] + ".wikia.com/wiki/File:" + urlReg[2];
6520 else if (!!(urlReg = source.match(/^https?:\/\/vignette(?:\d*)\.wikia\.nocookie\.net\/([^\/]+)\/images\/[a-f0-9]\/[a-f0-9]{2}\/([^\/]+)/i)))
6521 url = "http://" + urlReg[1] + ".wikia.com/wiki/File:" + urlReg[2];
6522 else if (!!(urlReg = source.match(/^http:\/\/(?:(?:\d{1,3}\.){3}\d{1,3}):(?:\d{1,5})\/h\/([a-f0-9]{40})-(?:\d+-){3}(?:png|gif|(?:jpe?g?))\/keystamp=\d+-[a-f0-9]{10}\/([^\/]+)/i)))
6523 url = "http://g.e-hentai.org/?f_shash=" + urlReg[1] + "&fs_from=" + urlReg[2];
6524 else if (!!(urlReg = source.match(/^http:\/\/e-shuushuu.net\/images\/\d{4}-(?:\d{2}-){2}(\d+)/i)))
6525 url = "http://e-shuushuu.net/image/" + urlReg[1];
6526 else if (!!(urlReg = source.match(/^http:\/\/jpg\.nijigen-daiaru\.com\/(\d+)/i)))
6527 url = "http://nijigen-daiaru.com/book.php?idb=" + urlReg[1];
6528 else if (!!(urlReg = source.match(/^https?:\/\/sozai\.doujinantena\.com\/contents_jpg\/([a-f0-9]{32})\//i)))
6529 url = "http://doujinantena.com/page.php?id=" + urlReg[1];
6530 else if (!!(urlReg = source.match(/^http:\/\/rule34-(?:data-\d{3}|images)\.paheal\.net\/(?:_images\/)?([a-f0-9]{32})/i)))
6531 url = "https://rule34.paheal.net/post/list/md5:" + urlReg[1] + "/1";
6532 else if (!!(urlReg = source.match(/^http:\/\/shimmie\.katawa-shoujo\.com\/image\/(\d+)/i)))
6533 url = "https://shimmie.katawa-shoujo.com/post/view/" + urlReg[1];
6534 else if (postInfo.md5 && !!(urlReg = source.match(/^http:\/\/(?:(?:(?:img\d?|cdn)\.)?rule34\.xxx|img\.booru\.org\/(?:rule34|r34))(?:\/(?:img\/rule34|r34))?\/{1,2}images\/\d+\/(?:[a-f0-9]{32}|[a-f0-9]{40})\./i)))
6535 url = "https://rule34.xxx/index.php?page=post&s=list&md5=" + postInfo.md5;
6536 else if (!!(urlReg = source.match(/^https?:\/\/(?:s3\.amazonaws\.com\/imgly_production|img\.ly\/system\/uploads)\/((?:\d{3}\/){3}|\d+\/)/i))) {
6537 id = parseInt((urlReg[1].replace(/[^0-9]/g, '')) || 0).bbbEncode62();
6538 url = "https://img.ly/" + id;
6539 }
6540 else if (!!(urlReg = source.match(/^(http:\/\/.+)\/diarypro\/d(?:ata\/upfile\/|iary\.cgi\?mode=image&upfile=)(\d+)/i)))
6541 url = urlReg[1] + "/diarypro/diary.cgi?no=" + urlReg[2];
6542 else if (!!(urlReg = source.match(/^http:\/\/i(?:\d)?\.minus\.com\/(?:i|j)([^\.]{12,})/i)))
6543 url = "http://minus.com/i/" + urlReg[1];
6544 else if (!!(urlReg = source.match(/^https?:\/\/pic0[1-4]\.nijie\.info\/nijie_picture\/(?:diff\/main\/)?\d+_(\d+)_(?:\d{10}|\d+_\d{14})/i)))
6545 url = "https://nijie.info/view.php?id=" + urlReg[1];
6546 else if (!!(urlReg = source.match(/^https?:\/\/(?:[^.]+\.)?yande\.re\/(?:image|jpeg|sample)\/[a-f0-9]{32}\/yande\.re%20(\d+)/i)))
6547 url = "https://yande.re/post/show/" + urlReg[1];
6548 else if (!!(urlReg = source.match(/^https?:\/\/(?:[^.]+\.)?yande\.re\/(?:image|jpeg|sample)\/([a-f0-9]{32})(?:\/yande\.re.*|\/?\.(?:jpg|png))$/i)))
6549 url = "https://yande.re/post?tags=md5:" + urlReg[1];
6550 else if (!!(urlReg = source.match(/^https?:\/\/(?:[^.]+\.)?konachan\.com\/(?:image|jpeg|sample)\/[a-f0-9]{32}\/Konachan\.com%20-%20(\d+)/i)))
6551 url = "https://konachan.com/post/show/" + urlReg[1];
6552 else if (!!(urlReg = source.match(/^https?:\/\/(?:[^.]+\.)?konachan\.com\/(?:image|jpeg|sample)\/([a-f0-9]{32})(?:\/Konachan\.com%20-%20.*|\/?\.(?:jpg|png))$/i)))
6553 url = "https://konachan.com/post?tags=md5:" + urlReg[1];
6554 else if (!!(urlReg = source.match(/^https?:\/\/\w+\.artstation.com\/(?:artwork|projects)\/([a-z0-9-]+)$/i)))
6555 url = "https://www.artstation.com/artwork/" + urlReg[1];
6556 else if (!!(urlReg = source.match(/^https?:\/\/(?:o|image-proxy-origin)\.twimg\.com\/\d\/proxy\.jpg\?t=(\w+)&/i))) {
6557 url = window.atob(urlReg[1]).match(/https?:\/\/[\x20-\x7e]+/i);
6558
6559 if (url[0]) {
6560 url = url[0];
6561
6562 if (url.match(/^https?:\/\/twitpic.com\/show\/large\/[a-z0-9]+/i))
6563 url = url.substring(0, url.lastIndexOf('.')).replace("show/large/", "");
6564 }
6565 else
6566 url = source;
6567 }
6568 else if (!!(urlReg = source.match(/^https?:\/\/\w+\.photozou\.jp\/pub\/\d+\/(\d+)\/photo\/(\d+)_.*$/i)))
6569 url = "https://photozou.jp/photo/show/" + urlReg[1] + "/" + urlReg[2];
6570 else if (!!(urlReg = source.match(/^https?:\/\/\w+\.?toranoana\.jp\/(?:popup_(?:bl)?img\d*|ec\/img)\/\d{2}\/\d{4}\/\d{2}\/\d{2}\/(\d+)/i)))
6571 url = "https://ec.toranoana.jp/tora_r/ec/item/" + urlReg[1] + "/";
6572 else if (!!(urlReg = source.match(/^https?:\/\/\w+\.hitomi\.la\/galleries\/(\d+)\/(?:page)?(\d+)\w*\.[a-z]+$/i)))
6573 url = "https://hitomi.la/reader/" + urlReg[1] + ".html#" + parseInt(urlReg[2], 10);
6574 else if (!!(urlReg = source.match(/^https?:\/\/\w+\.hitomi\.la\/galleries\/(\d+)\/\w*\.[a-z]+$/i)))
6575 url = "https://hitomi.la/galleries/" + urlReg[1] + ".html";
6576 else
6577 url = source;
6578
6579 return url;
6580 }
6581
6582 function fixPaginator(target) {
6583 // Determine whether the paginator needs to be updated and request one as needed.
6584 var paginator = getPaginator(target);
6585
6586 if (!paginator || gLoc === "pool" || gLoc === "favorite_group" || !allowUserLimit())
6587 return;
6588
6589 if (/\d/.test(paginator.textContent)) { // Fix numbered paginators.
6590 // Fix existing paginator with user's custom limit.
6591 var pageLinks = paginator.getElementsByTagName("a");
6592
6593 for (var i = 0, il = pageLinks.length; i < il; i++) {
6594 var pageLink = pageLinks[i];
6595 pageLink.href = updateURLQuery(pageLink.href, {limit: thumbnail_count});
6596 }
6597
6598 searchPages("paginator");
6599 }
6600 else { // Fix next/previous paginators.
6601 paginator.innerHTML = "<p>Loading...</p>"; // Disable the paginator while fixing it.
6602
6603 searchPages("paginator");
6604 }
6605 }
6606
6607 function fixCommentSearch() {
6608 // Fix the thumbnails for hidden posts in the comment search.
6609 var posts = getPosts();
6610
6611 for (var i = 0, il = posts.length; i < il; i++) {
6612 var post = posts[i];
6613 var postInfo = post.bbbInfo();
6614 var hasImg = post.getElementsByTagName("img")[0];
6615 var tags = postInfo.tag_string;
6616
6617 // Skip posts with content the user doesn't want or that already have images.
6618 if (hasImg || (!show_loli && /(?:^|\s)loli(?:$|\s)/.test(tags)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(tags)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(tags)) || (!show_banned && /(?:^|\s)banned(?:$|\s)/.test(postInfo.flags)) || safebPostTest(post))
6619 continue;
6620
6621 var preview = post.getElementsByClassName("preview")[0];
6622 var before = preview.firstElementChild;
6623
6624 var thumb = document.createElement("a");
6625 thumb.href = "/posts/" + postInfo.id;
6626 thumb.innerHTML = '<img src="' + postInfo.preview_img_src + '">';
6627
6628 if (before)
6629 preview.insertBefore(thumb, before);
6630 else
6631 preview.appendChild(thumb);
6632
6633 prepThumbnails(post);
6634 }
6635 }
6636
6637 function parseJson(jsonString, fallback) {
6638 // Try to parse a JSON string and catch any error that pops up.
6639 try {
6640 return JSON.parse(jsonString);
6641 }
6642 catch (error) {
6643 if (fallback && typeof(jsonString) === "string") {
6644 if (typeof(fallback) === "function")
6645 fallback(error, jsonString);
6646 else
6647 return fallback;
6648 }
6649 else
6650 throw error;
6651 }
6652 }
6653
6654 function jsonSettingsErrorHandler(error, jsonString) {
6655 // Default function for "bbb_settings" in parseJson that alerts the user of a problem and returns the default settings instead.
6656 function corruptSettingsDialog() {
6657 var textarea = document.createElement("textarea");
6658 textarea.style.width = "900px";
6659 textarea.style.height = "300px";
6660 textarea.value = "Corrupt Better Better Booru Settings (" + timestamp() + ") (Error: " + error.message + "):\r\n\r\n" + jsonString + "\r\n";
6661 bbbDialog(textarea);
6662 }
6663
6664 var linkId = uniqueIdNum();
6665 var noticeMsg = bbbNotice('BBB was unable to load your settings due to an unexpected error potentially caused by corrupted information. If you would like a copy of your corrupt settings, please click <a id="' + linkId + '" href="#">here</a>. (Error: ' + error.message + ')', -1);
6666
6667 document.getElementById(linkId).addEventListener("click", function(event) {
6668 if (event.button !== 0)
6669 return;
6670
6671 closeBbbNoticeMsg(noticeMsg);
6672 corruptSettingsDialog();
6673 event.preventDefault();
6674 }, false);
6675
6676 return defaultSettings();
6677 }
6678
6679 function bbbNotice(txt, noticeType) {
6680 // Display the notice or add information to it if it already exists.
6681 // A secondary number argument can be provided: -1 = error, 0 = permanent, >0 = temporary (disappears after X seconds where X equals the number provided), "no number" = temporary for 6 seconds
6682 var notice = bbb.el.notice;
6683 var noticeMsg = bbb.el.noticeMsg;
6684 var type = (typeof(noticeType) !== "undefined" ? noticeType : 6);
6685
6686 var msg = document.createElement("div");
6687 msg.className = "bbb-notice-msg-entry";
6688 msg.innerHTML = txt;
6689 msg.style.color = (type === -1 ? "#FF0000" : "#000000");
6690
6691 if (!notice) {
6692 var noticeContainer = document.createElement("span"); // Will contain the Danbooru notice and BBB notice so that they can "stack" and reposition as the other disappears.
6693 noticeContainer.id = "bbb-notice-container";
6694
6695 var danbNotice = document.getElementById("notice");
6696
6697 // Override Danbooru notice styling to make it get along with the notice container.
6698 if (danbNotice) {
6699 danbNotice.style.marginBottom = "10px";
6700 danbNotice.style.width = "100%";
6701 danbNotice.style.position = "relative";
6702 danbNotice.style.top = "0px";
6703 danbNotice.style.left = "0px";
6704 noticeContainer.appendChild(danbNotice);
6705 }
6706
6707 notice = bbb.el.notice = document.createElement("div");
6708 notice.id = "bbb-notice";
6709 notice.innerHTML = '<div style="position:absolute; left: 3px; font-weight: bold;">BBB:</div><div id="bbb-notice-msg"></div><div style="position: absolute; top: 3px; right: 3px; cursor: pointer;" class="close-button ui-icon ui-icon-closethick" id="bbb-notice-close"></div>';
6710 noticeContainer.appendChild(notice);
6711
6712 noticeMsg = bbb.el.noticeMsg = getId("bbb-notice-msg", notice);
6713
6714 getId("bbb-notice-close", notice).addEventListener("click", function(event) {
6715 if (event.button !== 0)
6716 return;
6717
6718 closeBbbNotice();
6719 event.preventDefault();
6720 }, false);
6721
6722 document.body.appendChild(noticeContainer);
6723 }
6724
6725 if (bbb.timers.keepBbbNotice)
6726 window.clearTimeout(bbb.timers.keepBbbNotice);
6727
6728 if (notice.style.display === "block" && /\S/.test(noticeMsg.textContent)) { // Insert new text at the top if the notice is already open and has an actual message.
6729 noticeMsg.insertBefore(msg, noticeMsg.firstElementChild);
6730
6731 // Don't allow the notice to be closed via clicking for half a second. Prevents accidental message closing.
6732 bbb.timers.keepBbbNotice = window.setTimeout(function() {
6733 bbb.timers.keepBbbNotice = 0;
6734 }, 500);
6735 }
6736 else { // Make sure the notice is clear and put in the first message.
6737 noticeMsg.innerHTML = "";
6738 noticeMsg.appendChild(msg);
6739 }
6740
6741 // Hide the notice after a certain number of seconds.
6742 if (type > 0) {
6743 window.setTimeout(function() {
6744 closeBbbNoticeMsg(msg);
6745 }, type * 1000);
6746 }
6747
6748 notice.style.display = "block";
6749
6750 return msg;
6751 }
6752
6753 function closeBbbNotice() {
6754 // Click handler for closing the notice.
6755 if (bbb.timers.keepBbbNotice)
6756 return;
6757
6758 bbb.el.notice.style.display = "none";
6759 }
6760
6761 function closeBbbNoticeMsg(el) {
6762 // Closes the provided notice message or the whole notice if there is only one message.
6763 var notice = bbb.el.notice;
6764 var target = el;
6765 var targetParent = target.parentNode;
6766
6767 if (notice.getElementsByClassName("bbb-notice-msg-entry").length < 2)
6768 closeBbbNotice();
6769 else if (targetParent)
6770 targetParent.removeChild(target);
6771 }
6772
6773 function bbbStatus(mode, xmlState) {
6774 // Updates the BBB status message.
6775 // xmlState: "new" = opening an XML request, "done" = closing an xml request, "error" = xml request failed.
6776 if (!enable_status_message)
6777 return;
6778
6779 var status = bbb.el.status;
6780
6781 // Set up the status message if it isn't ready.
6782 if (!status) {
6783 bbb.status = { // Status messages.
6784 msg: {
6785 post_comments: {txt: "Loading hidden comments... ", count: 0},
6786 posts: {txt: "Loading post info... ", count: 0} // General message for XML requests for hidden posts.
6787 },
6788 count: 0
6789 };
6790
6791 var msgList = bbb.status.msg;
6792
6793 status = bbb.el.status = document.createElement("div");
6794 status.id = "bbb-status";
6795
6796 for (var i in msgList) {
6797 if (msgList.hasOwnProperty(i)) {
6798 var curMsg = msgList[i];
6799
6800 var msgDiv = curMsg.el = document.createElement("div");
6801 msgDiv.style.display = "none";
6802 msgDiv.innerHTML = curMsg.txt;
6803 status.appendChild(msgDiv);
6804
6805 curMsg.info = document.createElement("span");
6806 msgDiv.appendChild(curMsg.info);
6807 }
6808 }
6809
6810 document.body.appendChild(status);
6811 }
6812
6813 var msg = bbb.status.msg[mode];
6814 var newCount = 0;
6815
6816 if (msg.queue) { // If the xml requests are queued, use the queue length as the current remaining value.
6817 newCount = (xmlState === "error" ? 0 : msg.queue.length);
6818 bbb.status.count += newCount - msg.count;
6819 msg.count = newCount;
6820 msg.info.innerHTML = newCount; // Update the displayed number of requests remaining.
6821 }
6822 else { // For simultaneous xml requests, just increment/decrement.
6823 if (xmlState === "new")
6824 newCount = 1;
6825 else if (xmlState === "done" || xmlState === "error")
6826 newCount = -1;
6827
6828 bbb.status.count += newCount;
6829 msg.count += newCount;
6830 }
6831
6832 if (msg.count)
6833 msg.el.style.display = "block";
6834 else
6835 msg.el.style.display = "none";
6836
6837 if (bbb.status.count) // If requests are pending, display the notice.
6838 status.style.display = "block";
6839 else // If requests are done, hide the notice.
6840 status.style.display = "none";
6841 }
6842
6843 function bbbDialog(content, properties) {
6844 // Open a dialog window that can have a predefined ok button (default) and/or cancel button. The properties object specifies dialog behavior and has the following values:
6845 // ok/cancel: true to display the button, false to hide the button, function to display the button and specify a custom function for it
6846 // condition: string to name a basic flag that will be checked/set by a dialog before displaying it, function to check custom conditions for a dialog before displaying it
6847 // important: true to prioritize a dialog if it goes in the queue, false to allow a dialog to go to the end of the queue as normal
6848
6849 var prop = properties || {};
6850 var okButton = (prop.ok === undefined ? true : prop.ok);
6851 var cancelButton = (prop.cancel === undefined ? false : prop.cancel);
6852 var condition = (prop.condition === undefined ? false : prop.condition);
6853 var important = (prop.important === undefined ? false : prop.important);
6854
6855 // Queue the dialog window if one is already open.
6856 if (document.getElementById("bbb-dialog-blocker")) {
6857 if (important)
6858 bbb.dialog.queue.unshift({content: content, properties: properties});
6859 else
6860 bbb.dialog.queue.push({content: content, properties: properties});
6861
6862 return;
6863 }
6864
6865 // Test whether the dialog window should be allowed to display.
6866 if (condition) {
6867 var conditionType = typeof(condition);
6868
6869 if ((conditionType === "string" && bbb.flags[condition]) || (conditionType === "function" && condition())) {
6870 nextBbbDialog();
6871 return;
6872 }
6873 else if (conditionType === "string")
6874 bbb.flags[condition] = true;
6875 }
6876
6877 // Create the dialog window.
6878 var blockDiv = document.createElement("div");
6879 blockDiv.id = "bbb-dialog-blocker";
6880
6881 var windowDiv = document.createElement("div");
6882 windowDiv.id = "bbb-dialog-window";
6883 windowDiv.tabIndex = "-1";
6884 blockDiv.appendChild(windowDiv);
6885
6886 var contentDiv = windowDiv;
6887
6888 if (okButton) {
6889 var ok = document.createElement("a");
6890 ok.innerHTML = "OK";
6891 ok.href = "#";
6892 ok.className = "bbb-dialog-button";
6893
6894 if (typeof(okButton) === "function")
6895 ok.addEventListener("click", okButton, false);
6896
6897 ok.addEventListener("click", closeBbbDialog, false);
6898
6899 okButton = ok;
6900 }
6901
6902 if (cancelButton) {
6903 var cancel = document.createElement("a");
6904 cancel.innerHTML = "Cancel";
6905 cancel.href = "#";
6906 cancel.className = "bbb-dialog-button";
6907 cancel.style.cssFloat = "right";
6908
6909 if (typeof(cancelButton) === "function")
6910 cancel.addEventListener("click", cancelButton, false);
6911
6912 cancel.addEventListener("click", closeBbbDialog, false);
6913
6914 cancelButton = cancel;
6915 }
6916
6917 if (okButton || cancelButton) {
6918 contentDiv = document.createElement("div");
6919 contentDiv.className = "bbb-dialog-content-div";
6920 windowDiv.appendChild(contentDiv);
6921
6922 var buttonDiv = document.createElement("div");
6923 buttonDiv.className = "bbb-dialog-button-div";
6924 windowDiv.appendChild(buttonDiv);
6925
6926 if (okButton)
6927 buttonDiv.appendChild(okButton);
6928
6929 if (cancelButton)
6930 buttonDiv.appendChild(cancelButton);
6931
6932 // Only allow left clicks to trigger the prompt buttons.
6933 buttonDiv.addEventListener("click", function(event) {
6934 if (event.button !== 0)
6935 event.stopPropagation();
6936 }, true);
6937 }
6938
6939 if (typeof(content) === "string")
6940 contentDiv.innerHTML = content;
6941 else
6942 contentDiv.appendChild(content);
6943
6944 document.body.appendChild(blockDiv);
6945
6946 (okButton || cancelButton || windowDiv).focus();
6947 }
6948
6949 function closeBbbDialog(event) {
6950 // Close the current dialog window.
6951 var dialogBlocker = document.getElementById("bbb-dialog-blocker");
6952
6953 if (dialogBlocker)
6954 document.body.removeChild(dialogBlocker);
6955
6956 nextBbbDialog();
6957
6958 event.preventDefault();
6959 }
6960
6961 function nextBbbDialog() {
6962 // Open the next queued dialog window.
6963 var nextDialog = bbb.dialog.queue.shift();
6964
6965 if (nextDialog)
6966 bbbDialog(nextDialog.content, nextDialog.properties);
6967 }
6968
6969 function thumbSearchMatch(post, searchArray) {
6970 // Take search objects and test them against a thumbnail's info.
6971 if (!searchArray[0])
6972 return false;
6973
6974 var postSearchInfo; // If/else variable.
6975
6976 if (post instanceof Element) {
6977 var postInfo = post.bbbInfo();
6978 var flags = postInfo.flags || "active";
6979 var rating = " rating:" + postInfo.rating;
6980 var status = " status:" + (flags === "flagged" ? flags + " active" : flags).replace(/\s/g, " status:");
6981 var user = (postInfo.uploader_name ? " user:" + postInfo.uploader_name.replace(/\s/g, "_").toLowerCase() : "");
6982 var userId = " userid:" + postInfo.uploader_id;
6983 var taggerId = " taggerid:" + postInfo.keeper_data.uid;
6984 var approverId = (postInfo.approver_id ? " approverid:" + postInfo.approver_id : "");
6985 var source = (postInfo.source ? " source:" + postInfo.source + " source:" + postInfo.normalized_source : "");
6986 var pools = " " + (/pool:\d+/.test(postInfo.pool_string) && !/pool:(collection|series)/.test(postInfo.pool_string) ? postInfo.pool_string + " pool:inactive" : postInfo.pool_string);
6987 var parent = (postInfo.parent_id ? " parent:" + postInfo.parent_id : "");
6988 var child = (postInfo.has_children === true ? " child:true" : "");
6989 var isFav = " isfav:" + postInfo.is_favorited;
6990 var filetype = " filetype:" + postInfo.file_ext;
6991
6992 postSearchInfo = {
6993 tags: postInfo.tag_string.bbbSpacePad(),
6994 metatags:(rating + status + user + pools + parent + child + isFav + userId + taggerId + source + approverId + filetype).bbbSpacePad(),
6995 score: postInfo.score,
6996 favcount: postInfo.fav_count,
6997 id: postInfo.id,
6998 width: postInfo.image_width,
6999 height: postInfo.image_height
7000 };
7001 }
7002 else
7003 postSearchInfo = post;
7004
7005 var j, jl, searchTerm; // Loop variables.
7006
7007 for (var i = 0, il = searchArray.length; i < il; i++) {
7008 var searchObject = searchArray[i];
7009 var all = searchObject.all;
7010 var any = searchObject.any;
7011
7012 // Continue to the next matching rule if there are no tags to test.
7013 if (!any.total && !all.total)
7014 continue;
7015
7016 if (any.total) {
7017 var anyResult = false;
7018
7019 // Loop until one positive match is found.
7020 for (j = 0, jl = any.includes.length; j < jl; j++) {
7021 searchTerm = any.includes[j];
7022
7023 if (thumbTagMatch(postSearchInfo, searchTerm)) {
7024 anyResult = true;
7025 break;
7026 }
7027 }
7028
7029 // If we don't have a positive match yet, loop through the excludes.
7030 if (!anyResult) {
7031 for (j = 0, jl = any.excludes.length; j < jl; j++) {
7032 searchTerm = any.excludes[j];
7033
7034 if (!thumbTagMatch(postSearchInfo, searchTerm)) {
7035 anyResult = true;
7036 break;
7037 }
7038 }
7039 }
7040
7041 // Continue to the next matching rule if none of the "any" tags matched.
7042 if (!anyResult)
7043 continue;
7044 }
7045
7046 if (all.total) {
7047 var allResult = true;
7048
7049 // Loop until a negative match is found.
7050 for (j = 0, jl = all.includes.length; j < jl; j++) {
7051 searchTerm = all.includes[j];
7052
7053 if (!thumbTagMatch(postSearchInfo, searchTerm)) {
7054 allResult = false;
7055 break;
7056 }
7057 }
7058
7059 // If we still have a positive match, loop through the excludes.
7060 if (allResult) {
7061 for (j = 0, jl = all.excludes.length; j < jl; j++) {
7062 searchTerm = all.excludes[j];
7063
7064 if (thumbTagMatch(postSearchInfo, searchTerm)) {
7065 allResult = false;
7066 break;
7067 }
7068 }
7069 }
7070
7071 // Continue to the next matching rule if one of the "all" tags didn't match.
7072 if (!allResult)
7073 continue;
7074 }
7075
7076 // Loop completed without a negative match so return true.
7077 return true;
7078 }
7079
7080 // If we haven't managed a positive match for any rules, return false.
7081 return false;
7082 }
7083
7084 function thumbTagMatch(postSearchInfo, tag) {
7085 // Test thumbnail info for a tag match.
7086 var targetTags; // If/else variable.
7087
7088 if (typeof(tag) === "string") { // Check regular tags and metatags with string values.
7089 targetTags = (isMetatag(tag) ? postSearchInfo.metatags : postSearchInfo.tags);
7090
7091 if (targetTags.indexOf(tag) > -1)
7092 return true;
7093 else
7094 return false;
7095 }
7096 else if (tag instanceof RegExp) { // Check wildcard tags.
7097 targetTags = (isMetatag(tag.source) ? postSearchInfo.metatags : postSearchInfo.tags);
7098
7099 return tag.test(targetTags);
7100 }
7101 else if (typeof(tag) === "object") {
7102 if (tag instanceof Array) // Check grouped tags.
7103 return thumbSearchMatch(postSearchInfo, tag);
7104 else { // Check numeric metatags.
7105 var tagsMetaValue = postSearchInfo[tag.tagName];
7106
7107 if (tag.equals !== undefined) {
7108 if (tagsMetaValue !== tag.equals)
7109 return false;
7110 }
7111 else {
7112 if (tag.greater !== undefined && tagsMetaValue <= tag.greater)
7113 return false;
7114
7115 if (tag.less !== undefined && tagsMetaValue >= tag.less)
7116 return false;
7117 }
7118
7119 return true;
7120 }
7121 }
7122 }
7123
7124 function createSearch(string) {
7125 // Take search strings, turn them into search objects, and pass back the objects in an array.
7126 if (!/[^\s,]/.test(string))
7127 return [];
7128
7129 var searchString = string;
7130 var i, il; // Loop variables.
7131
7132 // Group handling.
7133 searchString = replaceSearchGroupMetatag(searchString);
7134
7135 var groupsObject = replaceSearchGroups(searchString);
7136 var groups = groupsObject.groups;
7137 var searchStrings = groupsObject.search.toLowerCase().replace(/\b(rating:[qes])\w+/g, "$1").split(",");
7138 var searches = [];
7139
7140 // Sort through each matching rule.
7141 for (i = 0, il = searchStrings.length; i < il; i++) {
7142 var searchTerms = searchStrings[i].split(" ");
7143 var searchObject = {
7144 all: {includes: [], excludes: [], total: 0},
7145 any: {includes: [], excludes: [], total: 0}
7146 };
7147
7148 // Divide the tags into any and all sets with excluded and included tags.
7149 for (var j = 0, jl = searchTerms.length; j < jl; j++) {
7150 var searchTerm = searchTerms[j];
7151 var primaryMode = "all";
7152 var secondaryMode = "includes";
7153
7154 while (searchTerm.charAt(0) === "~" || searchTerm.charAt(0) === "-") {
7155 switch (searchTerm.charAt(0)) {
7156 case "~":
7157 primaryMode = "any";
7158 break;
7159 case "-":
7160 secondaryMode = "excludes";
7161 break;
7162 }
7163
7164 searchTerm = searchTerm.slice(1);
7165 }
7166
7167 if (!searchTerm.length) // Stop if there is no actual tag.
7168 continue;
7169
7170 var mode = searchObject[primaryMode][secondaryMode];
7171
7172 if (isNumMetatag(searchTerm)) { // Parse numeric metatags and turn them into objects.
7173 var tagArray = searchTerm.split(":");
7174 var metaObject = {
7175 tagName: tagArray[0],
7176 equals: undefined,
7177 greater: undefined,
7178 less: undefined
7179 };
7180 var numSearch = tagArray[1];
7181 var numArray, equals, greater, less; // If/else variables.
7182
7183 if (numSearch.indexOf("<=") === 0 || numSearch.indexOf("..") === 0) { // Less than or equal to. (tag:<=# & tag:..#)
7184 less = parseInt(numSearch.slice(2), 10);
7185
7186 if (!isNaN(less)) {
7187 metaObject.less = less + 1;
7188 mode.push(metaObject);
7189 }
7190 }
7191 else if (numSearch.indexOf(">=") === 0) { // Greater than or equal to. (tag:>=#)
7192 greater = parseInt(numSearch.slice(2), 10);
7193
7194 if (!isNaN(greater)) {
7195 metaObject.greater = greater - 1;
7196 mode.push(metaObject);
7197 }
7198 }
7199 else if (numSearch.length > 2 && numSearch.indexOf("..") === numSearch.length - 2) { // Greater than or equal to. (tag:#..)
7200 greater = parseInt(numSearch.slice(0, -2), 10);
7201
7202 if (!isNaN(greater)) {
7203 metaObject.greater = greater - 1;
7204 mode.push(metaObject);
7205 }
7206 }
7207 else if (numSearch.charAt(0) === "<") { // Less than. (tag:<#)
7208 less = parseInt(numSearch.slice(1), 10);
7209
7210 if (!isNaN(less)) {
7211 metaObject.less = less;
7212 mode.push(metaObject);
7213 }
7214 }
7215 else if (numSearch.charAt(0) === ">") { // Greater than. (tag:>#)
7216 greater = parseInt(numSearch.slice(1), 10);
7217
7218 if (!isNaN(greater)) {
7219 metaObject.greater = greater;
7220 mode.push(metaObject);
7221 }
7222 }
7223 else if (numSearch.indexOf("..") > -1) { // Greater than or equal to and less than or equal to range. (tag:#..#)
7224 numArray = numSearch.split("..");
7225 greater = parseInt(numArray[0], 10);
7226 less = parseInt(numArray[1], 10);
7227
7228 if (!isNaN(greater) && !isNaN(less)) {
7229 metaObject.greater = greater - 1;
7230 metaObject.less = less + 1;
7231 mode.push(metaObject);
7232 }
7233 }
7234 else { // Exact number. (tag:#)
7235 equals = parseInt(numSearch, 10);
7236
7237 if (!isNaN(equals)) {
7238 metaObject.equals = equals;
7239 mode.push(metaObject);
7240 }
7241 }
7242 }
7243 else if (searchTerm.indexOf("*") > -1) // Prepare wildcard tags as regular expressions.
7244 mode.push(new RegExp(escapeRegEx(searchTerm).replace(/\*/g, "\S*").bbbSpacePad())); // Don't use "\\S*" here since escapeRegEx replaces * with \*. That escape carries over to the next replacement and makes us end up with "\\S*".
7245 else if (/%\d+%/.test(searchTerm)) { // Prepare grouped tags as a search object.
7246 var groupIndex = Number(searchTerm.match(/\d+/)[0]);
7247
7248 mode.push(createSearch(groups[groupIndex]));
7249 }
7250 else if (typeof(searchTerm) === "string") { // Add regular tags.
7251 if (isMetatag(searchTerm)) {
7252 var tagObject = searchTerm.split(/:(.+)/, 2);
7253 var tagName = tagObject[0];
7254 var tagValue = tagObject[1];
7255
7256 // Drop metatags with no value.
7257 if (!tagValue)
7258 continue;
7259
7260 if (tagValue === "any" && (tagName === "pool" || tagName === "parent" || tagName === "child" || tagName === "source" || tagName === "approverid"))
7261 mode.push(new RegExp((tagName + ":\\S*").bbbSpacePad()));
7262 else if (tagValue === "none" && (tagName === "pool" || tagName === "parent" || tagName === "child" || tagName === "source" || tagName === "approverid")) {
7263 secondaryMode = (secondaryMode === "includes" ? "excludes" : "includes"); // Flip the include/exclude mode.
7264 mode = searchObject[primaryMode][secondaryMode];
7265
7266 mode.push(new RegExp((tagName + ":\\S*").bbbSpacePad()));
7267 }
7268 else if (tagValue === "active" && tagName === "pool")
7269 mode.push(new RegExp((tagName + ":(collection|series)").bbbSpacePad()));
7270 else if (tagName === "source") // Append a wildcard to the end of "sources starting with the given value" searches.
7271 mode.push(new RegExp((escapeRegEx(searchTerm) + "\\S*").bbbSpacePad()));
7272 else // Allow all other values through (ex: parent:# & pool:series).
7273 mode.push(searchTerm.bbbSpacePad());
7274 }
7275 else
7276 mode.push(searchTerm.bbbSpacePad());
7277 }
7278 }
7279
7280 searchObject.all.total = searchObject.all.includes.length + searchObject.all.excludes.length;
7281 searchObject.any.total = searchObject.any.includes.length + searchObject.any.excludes.length;
7282
7283 if (searchObject.all.total || searchObject.any.total)
7284 searches.push(searchObject);
7285 }
7286
7287 return searches;
7288 }
7289
7290 function replaceSearchGroupMetatag(string) {
7291 // Replace group metatags in a search string with their respective tags.
7292 var searchString = string;
7293
7294 if (string.indexOf("g:") > -1 || string.indexOf("group:") > -1) {
7295 loadMetaGroups();
7296
7297 var groupMetaRegEx = /((?:^|\s)[-~]*)g(?:roup)?:([^\s,]+)/;
7298 var groupMetaRecursion = 1;
7299 var groupMetaMatches;
7300
7301 while (groupMetaRecursion <= 99 && (groupMetaMatches = searchString.match(groupMetaRegEx))) {
7302 searchString = searchString.replace(groupMetaMatches[0], groupMetaMatches[1] + "( " + (bbb.groups[groupMetaMatches[2]] || "") + " )");
7303 groupMetaRecursion++;
7304 }
7305
7306 if (groupMetaRecursion > 99 && groupMetaRegEx.test(searchString)) {
7307 var recursiveGroupName = searchString.match(groupMetaRegEx)[0].split(":")[1];
7308
7309 bbbNotice("While searching thumbnails, BBB encountered what appears to be an infinite loop in your groups. Please check you group titled \"" + recursiveGroupName + "\" to see if it references itself or other groups that eventually end up referencing back to it.", -1);
7310 bbb.groups[recursiveGroupName] = ""; // Disable the group.
7311 }
7312 }
7313
7314 return searchString;
7315 }
7316
7317 function replaceSearchGroups(string) {
7318 // Collect all the nested/grouped tags in a search string and replace them with placeholders.
7319 var searchString = string;
7320
7321 if (string.indexOf("BBBPARENS") < 0) {
7322 // Replace parentheses that are not part of a tag with placeholders.
7323 var parensRegex = /(?:^|([\s,]))([-~]*)\(%?(?=$|[\s,])|(?:^|([\s,]))%?\)(?=$|[\s,])/;
7324
7325 if (!parensRegex.test(string))
7326 return {search: string, groups: []};
7327
7328 var parensMatches;
7329
7330 while ((parensMatches = searchString.match(parensRegex))) {
7331 var parensMatch = parensMatches[0];
7332 var parensOperators = parensMatches[2] || "";
7333 var parensPreceding = parensMatches[1] || parensMatches[3] || "";
7334
7335 searchString = searchString.replace(parensMatch, parensPreceding + (parensMatch.indexOf("(") > -1 ? parensOperators + "BBBPARENSOPEN" : "BBBPARENSCLOSE"));
7336 }
7337 }
7338
7339 var parens = searchString.match(/BBBPARENSOPEN|BBBPARENSCLOSE/g);
7340
7341 // Remove unpaired opening parentheses near the end of the search.
7342 while (parens[parens.length - 1] === "BBBPARENSOPEN") {
7343 searchString = searchString.replace(/^(.*\s)?[~-]*BBBPARENSOPEN/, "$1");
7344 parens.pop();
7345 }
7346
7347 // Take the remaining parentheses and figure out how to pair them up.
7348 var startCount = 0;
7349 var endCount = 0;
7350 var groupStartIndex = 0;
7351 var groups = [];
7352
7353 for (var i = 0, il = parens.length; i < il; i++) {
7354 var paren = parens[i];
7355 var nextParen = parens[i + 1];
7356
7357 if (paren === "BBBPARENSOPEN")
7358 startCount++;
7359 else
7360 endCount++;
7361
7362 if (endCount > startCount) { // Remove unpaired closing parentheses near the start of the string.
7363 searchString = searchString.replace(/^(.*?)BBBPARENSCLOSE/, "$1");
7364 endCount = 0;
7365 groupStartIndex++;
7366 }
7367 else if (startCount === endCount || (!nextParen && endCount > 0 && startCount > endCount)) { // Replace evenly paired parentheses with a placeholder.
7368 var groupRegex = new RegExp(parens.slice(groupStartIndex, i + 1).join(".*?"));
7369 var groupMatch = searchString.match(groupRegex)[0];
7370
7371 searchString = searchString.replace(groupMatch, "%" + groups.length + "%");
7372 startCount = 0;
7373 endCount = 0;
7374 groupStartIndex = i + 1;
7375 groups.push(groupMatch.substring(13, groupMatch.length - 14));
7376 }
7377 else if (!nextParen && startCount > 0 && endCount === 0 ) // Remove leftover unpaired opening parentheses.
7378 searchString = searchString.replace(/^(.*\s)?[~-]*BBBPARENSOPEN/, "$1");
7379 }
7380
7381 return {search: searchString, groups: groups};
7382 }
7383
7384 function restoreSearchGroups(string, groups) {
7385 // Replace all group placeholders with their corresponding group and restore leftover parenthesis placeholders.
7386 var searchString = string;
7387
7388 for (var i = 0, il = groups.length; i < il; i++) {
7389 var groupPlaceholder = new RegExp("%" + i + "%");
7390
7391 searchString = searchString.replace(groupPlaceholder, "( " + groups[i].bbbSpaceClean() + " )");
7392 }
7393
7394 return searchString.replace(/BBBPARENSOPEN/g, "(").replace(/BBBPARENSCLOSE/g, ")");
7395 }
7396
7397 function cleanSearchGroups(string) {
7398 // Take a search string and clean up extra spaces, commas, and any parentheses that are missing their opening/closing parenthesis.
7399 var groupObject = replaceSearchGroups(string);
7400 var groups = groupObject.groups;
7401 var searchString = groupObject.search;
7402
7403 for (var i = 0, il = groups.length; i < il; i++)
7404 groups[i] = cleanSearchGroups(groups[i]);
7405
7406 searchString = restoreSearchGroups(searchString, groups).bbbTagClean();
7407
7408 return searchString;
7409 }
7410
7411 function searchSingleToMulti(single) {
7412 // Take a single line search and format it into multiple lines for a textarea.
7413 var groupsObject = replaceSearchGroups(cleanSearchGroups(single));
7414 var searchString = groupsObject.search;
7415 var groups = groupsObject.groups;
7416 var multi = searchString.replace(/,\s*/g, " \r\n\r\n");
7417
7418 multi = restoreSearchGroups(multi, groups);
7419
7420 return multi;
7421 }
7422
7423 function searchMultiToSingle(multi) {
7424 // Take a multiple line search from a textarea and format it into a single line.
7425 var searchStrings = multi.split(/[\r\n]+/g);
7426
7427 for (var i = 0, il = searchStrings.length; i < il; i++)
7428 searchStrings[i] = cleanSearchGroups(searchStrings[i]);
7429
7430 var single = searchStrings.join(", ").bbbTagClean();
7431
7432 return single;
7433 }
7434
7435 function trackNew() {
7436 // Set up the track new option and manage the search.
7437 var header = document.getElementById("top");
7438
7439 if (!track_new || !header)
7440 return;
7441
7442 var activeMenu = header.getElementsByClassName("current")[0];
7443 var listingItem = document.getElementById("subnav-listing");
7444
7445 // Insert new posts link.
7446 if (activeMenu && activeMenu.id === "nav-posts" && listingItem) {
7447 var secondMenu = listingItem.parentNode;
7448 var listingItemSibling = listingItem.nextElementSibling;
7449
7450 var link = document.createElement("a");
7451 link.href = "/posts?new_posts=redirect&page=b1";
7452 link.innerHTML = "New";
7453 link.addEventListener("click", function(event) {
7454 if (event.button !== 0)
7455 return;
7456
7457 trackNewLoad();
7458 event.preventDefault();
7459 }, false);
7460
7461 var item = document.createElement("li");
7462 item.appendChild(link);
7463
7464 if (listingItemSibling)
7465 secondMenu.insertBefore(item, listingItemSibling);
7466 else
7467 secondMenu.appendChild(item);
7468 }
7469
7470 if (gLoc === "search") {
7471 var info = track_new_data;
7472 var mode = getVar("new_posts");
7473 var postsDiv = document.getElementById("posts");
7474 var postSections = document.getElementById("post-sections");
7475 var firstPost = getPosts()[0];
7476
7477 if (mode === "init" && !info.viewed && !getVar("tags") && !getVar("page")) { // Initialize.
7478 if (firstPost) {
7479 info.viewed = Number(firstPost.bbbInfo("id"));
7480 info.viewing = 1;
7481 saveSettings();
7482 bbbNotice("New post tracking initialized. Tracking will start with new posts after the current last image.", 8);
7483 }
7484 }
7485 else if (mode === "redirect") { // Bookmarkable redirect link. (http://danbooru.donmai.us/posts?new_posts=redirect&page=b1)
7486 if (postsDiv)
7487 postsDiv.innerHTML = "<b>Redirecting...</b>";
7488
7489 trackNewLoad();
7490 }
7491 else if (mode === "list") {
7492 var limitNum = getLimit() || thumbnail_count || thumbnail_count_default;
7493 var currentPage = Number(getVar("page")) || 1;
7494 var savedPage = Math.ceil((info.viewing - limitNum) / limitNum) + 1;
7495 var currentViewed = Number(decodeURIComponent(location.search).match(/id:>(\d+)/)[1]);
7496 var paginator = getPaginator();
7497
7498 // Replace the chickens message on the first page with a more specific message.
7499 if (!firstPost && currentPage < 2) {
7500 if (postsDiv && postsDiv.firstElementChild)
7501 postsDiv.firstElementChild.innerHTML = "No new posts.";
7502 }
7503
7504 // Update the saved page information.
7505 if (savedPage !== currentPage && info.viewed === currentViewed) {
7506 info.viewing = (currentPage - 1) * limitNum + 1;
7507 saveSettings();
7508 }
7509
7510 // Modify new post searches with a mark as viewed link.
7511 if (postSections) {
7512 var markSection = document.createElement("li");
7513
7514 var markLink = document.createElement("a");
7515 markLink.innerHTML = (currentPage > 1 ? "Mark pages 1-" + currentPage + " viewed" : "Mark page 1 viewed");
7516 markLink.href = "#";
7517 markSection.appendChild(markLink);
7518 postSections.appendChild(markSection);
7519
7520 markLink.addEventListener("click", function(event) {
7521 if (event.button !== 0)
7522 return;
7523
7524 trackNewMark();
7525 event.preventDefault();
7526 }, false);
7527
7528 var resetSection = document.createElement("li");
7529 resetSection.style.cssFloat = "right";
7530
7531 var resetLink = document.createElement("a");
7532 resetLink.innerHTML = "Reset (Mark all viewed)";
7533 resetLink.href = "#";
7534 resetLink.style.color = "#FF1100";
7535 resetSection.appendChild(resetLink);
7536 postSections.appendChild(resetSection);
7537
7538 resetLink.addEventListener("click", function(event) {
7539 if (event.button !== 0)
7540 return;
7541
7542 trackNewReset();
7543 event.preventDefault();
7544 }, false);
7545
7546 // Update the mark link if the paginator updates.
7547 if (paginator) {
7548 paginator.bbbWatchNodes(function() {
7549 var activePage = paginator.getElementsByClassName("current-page")[0];
7550 var pageNumber = (activePage ? activePage.textContent.bbbSpaceClean() : "1") || "1";
7551
7552 if (pageNumber && pageNumber !== "1")
7553 markLink.innerHTML = "Mark pages 1-" + pageNumber + " viewed";
7554 });
7555 }
7556 }
7557 }
7558 }
7559 }
7560
7561 function trackNewLoad() {
7562 // Create the search URL and load it.
7563 var info = bbb.user.track_new_data;
7564 var limitNum = bbb.user.thumbnail_count || thumbnail_count_default;
7565 var savedPage = Math.ceil((info.viewing - limitNum) / limitNum) + 1;
7566
7567 if (info.viewed)
7568 location.href = "/posts?new_posts=list&tags=order:id_asc+id:>" + info.viewed + "&page=" + savedPage + "&limit=" + limitNum;
7569 else
7570 location.href = "/posts?new_posts=init&limit=" + limitNum;
7571 }
7572
7573 function trackNewReset() {
7574 // Reinitialize settings/Mark all viewed.
7575 loadSettings();
7576
7577 var limitNum = bbb.user.thumbnail_count || thumbnail_count_default;
7578
7579 bbb.user.track_new_data = bbb.options.track_new_data.def;
7580 saveSettings();
7581
7582 bbbNotice("Reinitializing new post tracking. Please wait.", 0);
7583 location.href = "/posts?new_posts=init&limit=" + limitNum;
7584 }
7585
7586 function trackNewMark() {
7587 // Mark the current images and older as viewed.
7588 loadSettings();
7589
7590 var info = bbb.user.track_new_data;
7591 var limitNum = getLimit() || bbb.user.thumbnail_count || thumbnail_count_default;
7592 var posts = getPosts();
7593 var lastPost = posts[posts.length - 1];
7594 var lastId = (lastPost ? Number(lastPost.bbbInfo("id")) : null);
7595
7596 if (!lastPost)
7597 bbbNotice("Unable to mark as viewed. No posts detected.", -1);
7598 else if (info.viewed >= lastId)
7599 bbbNotice("Unable to mark as viewed. Posts have already been marked.", -1);
7600 else {
7601 info.viewed = Number(lastPost.bbbInfo("id"));
7602 info.viewing = 1;
7603 saveSettings();
7604
7605 bbbNotice("Posts marked as viewed. Please wait while the pages are updated.", 0);
7606 location.href = "/posts?new_posts=list&tags=order:id_asc+id:>" + info.viewed + "&page=1&limit=" + limitNum;
7607 }
7608 }
7609
7610 function customCSS() {
7611 var i; // Loop variable.
7612 var customStyles = document.createElement("style");
7613 customStyles.type = "text/css";
7614
7615 var styles = '#bbb-menu {background-color: #FFFFFF; border: 1px solid #CCCCCC; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5); padding: 15px; position: fixed; top: 25px; left: 50%; z-index: 9001;}' +
7616 '#bbb-menu *, #bbb-dialog-window * {font-size: 14px; line-height: 16px; outline: 0px none; border: 0px none; margin: 0px; padding: 0px;}' + // Reset some base settings.
7617 '#bbb-menu h1, #bbb-dialog-window h1 {font-size: 24px; line-height: 42px;}' +
7618 '#bbb-menu h2, #bbb-dialog-window h2 {font-size: 16px; line-height: 25px;}' +
7619 '#bbb-menu input, #bbb-menu select, #bbb-menu textarea, #bbb-dialog-window input, #bbb-dialog-window select, #bbb-dialog-window textarea {border: #CCCCCC 1px solid;}' +
7620 '#bbb-menu input {height: 18px; padding: 1px 0px; margin-top: 4px; vertical-align: top;}' +
7621 '#bbb-menu input[type="checkbox"] {margin: 0px; vertical-align: middle; position: relative; bottom: 2px;}' +
7622 '#bbb-menu .bbb-general-input input[type="text"], #bbb-menu .bbb-general-input select {width: 175px;}' +
7623 '#bbb-menu select {height: 21px; margin-top: 4px; vertical-align: top;}' +
7624 '#bbb-menu option {padding: 0px 3px;}' +
7625 '#bbb-menu textarea, #bbb-dialog-window textarea {padding: 2px; resize: none;}' +
7626 '#bbb-menu ul, #bbb-menu ol, #bbb-dialog-window ul, #bbb-dialog-window ol {list-style: outside disc none; margin-top: 0px; margin-bottom: 0px; margin-left: 20px; display: block;}' +
7627 '#bbb-menu .bbb-scroll-div {border: 1px solid #CCCCCC; margin: -1px 0px 5px 0px; padding: 5px 0px; overflow-y: auto;}' +
7628 '#bbb-menu .bbb-page {position: relative; display: none;}' +
7629 '#bbb-menu .bbb-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 5px;}' +
7630 '#bbb-menu .bbb-tab {border-top-left-radius: 5px; border-top-right-radius: 5px; display: inline-block; padding: 5px; border: 1px solid #CCCCCC; margin-right: -1px;}' +
7631 '#bbb-menu .bbb-active-tab {background-color: #FFFFFF; border-bottom-width: 0px; padding-bottom: 6px;}' +
7632 '#bbb-menu .bbb-header {border-bottom: 2px solid #CCCCCC; margin-bottom: 5px; width: 700px;}' +
7633 '#bbb-menu .bbb-toc {list-style-type: upper-roman; margin-left: 30px;}' +
7634 '#bbb-menu .bbb-section-options, #bbb-menu .bbb-section-text {margin-bottom: 5px; max-width: 902px;}' +
7635 '#bbb-menu .bbb-section-options-left, #bbb-menu .bbb-section-options-right {display: inline-block; vertical-align: top; width: 435px;}' +
7636 '#bbb-menu .bbb-section-options-left {border-right: 1px solid #CCCCCC; margin-right: 15px; padding-right: 15px;}' +
7637 '#bbb-menu .bbb-general-label {display: block; height: 30px; padding: 0px 5px;}' +
7638 '#bbb-menu .bbb-general-label:hover {background-color: #EEEEEE;}' +
7639 '#bbb-menu .bbb-general-text {line-height: 30px;}' +
7640 '#bbb-menu .bbb-general-input {float: right; line-height: 30px;}' +
7641 '#bbb-menu .bbb-expl-link {font-size: 12px; font-weight: bold; margin-left: 5px; padding: 2px;}' +
7642 '#bbb-menu .bbb-list-div {background-color: #EEEEEE; padding: 2px; margin: 0px 5px 0px 0px;}' +
7643 '#bbb-menu .bbb-list-bar, #bbb-menu .bbb-list-settings {height: 30px; padding: 0px 2px; overflow: hidden;}' +
7644 '#bbb-menu .bbb-list-settings {background-color: #FFFFFF;}' +
7645 '#bbb-menu .bbb-list-div label, #bbb-menu .bbb-list-div span {display: inline-block; line-height: 30px;}' +
7646 '#bbb-menu .bbb-list-border-name {text-align: left; width: 540px;}' +
7647 '#bbb-menu .bbb-list-border-name input {width:460px;}' +
7648 '#bbb-menu .bbb-list-border-color {text-align: center; width: 210px;}' +
7649 '#bbb-menu .bbb-list-border-color input {width: 148px;}' +
7650 '#bbb-menu .bbb-list-border-style {float: right; text-align: right; width: 130px;}' +
7651 '#bbb-menu .bbb-list-group-name {text-align: left; width: 300px;}' +
7652 '#bbb-menu .bbb-list-group-name input {width:240px;}' +
7653 '#bbb-menu .bbb-list-group-tags {float: right; text-align: center; width: 100%;}' +
7654 '#bbb-menu .bbb-list-group-tags input {width:825px;}' +
7655 '#bbb-menu .bbb-list-divider {height: 4px;}' +
7656 '#bbb-menu .bbb-insert-highlight .bbb-list-divider {background-color: blue; cursor: pointer;}' +
7657 '#bbb-menu .bbb-no-highlight .bbb-list-divider {background-color: transparent; cursor: auto;}' +
7658 '#bbb-menu .bbb-list-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 2px; margin: 0px 2px;}' +
7659 '#bbb-menu .bbb-list-spacer {display: inline-block; height: 12px; width: 0px; border-right: 1px solid #CCCCCC; margin: 0px 5px;}' +
7660 '#bbb-menu .bbb-backup-area {height: 300px; width: 896px; margin-top: 2px;}' +
7661 '#bbb-menu .bbb-blacklist-area {height: 300px; width: 896px; margin-top: 2px;}' +
7662 '#bbb-menu .bbb-edit-link {background-color: #FFFFFF; border: 1px solid #CCCCCC; display: inline-block; height: 20px; line-height: 20px; margin-left: -1px; padding: 0px 2px; margin-top: 4px; text-align: center; vertical-align: top;}' +
7663 '#bbb-expl {background-color: #CCCCCC; border: 1px solid #000000; display: none; font-size: 12px; padding: 5px; position: fixed; max-width: 488px; width: 488px; overflow: hidden; z-index: 9002; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5);}' +
7664 '#bbb-expl * {font-size: 12px;}' +
7665 '#bbb-expl tiphead {display: block; font-weight: bold; text-decoration: underline; font-size: 13px; margin-top: 12px;}' +
7666 '#bbb-expl tipdesc {display: inline; font-weight: bold;}' +
7667 '#bbb-expl tipdesc:before {content: "\\A0"; display: block; height: 12px; clear: both;}' + // Simulate a double line break.
7668 '#bbb-status {background-color: rgba(255, 255, 255, 0.75); border: 1px solid rgba(204, 204, 204, 0.75); font-size: 12px; font-weight: bold; text-align: right; display: none; padding: 3px; position: fixed; bottom: 0px; right: 0px; z-index: 9002;}' +
7669 '#bbb-notice-container {position: fixed; top: 0.5em; left: 25%; width: 50%; z-index: 9002;}' +
7670 '#bbb-notice {padding: 3px; width: 100%; display: none; position: relative; border-radius: 2px; border: 1px solid #000000; background-color: #CCCCCC;}' +
7671 '#bbb-notice-msg {margin: 0px 25px 0px 55px; max-height: 200px; overflow: auto;}' +
7672 '#bbb-notice-msg .bbb-notice-msg-entry {border-bottom: solid 1px #000000; margin-bottom: 5px; padding-bottom: 5px;}' +
7673 '#bbb-notice-msg .bbb-notice-msg-entry:last-child {border-bottom: none 0px; margin-bottom: 0px; padding-bottom: 0px;}' +
7674 '#bbb-dialog-blocker {display: block; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.33); z-index: 9003; text-align: center;}' +
7675 '#bbb-dialog-blocker:before {content: ""; display: inline-block; height: 100%; vertical-align: middle;}' + // Helps vertically center an element with unknown dimensions: https://css-tricks.com/centering-in-the-unknown/
7676 '#bbb-dialog-window {display: inline-block; display: inline-flex; flex-flow: column; position: relative; background-color: #FFFFFF; border: 10px solid #FFFFFF; outline: 1px solid #CCC; box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.5); color: #000000; max-width: 940px; max-height: 90%; overflow-x: hidden; overflow-y: auto; text-align: left; vertical-align: middle; line-height: initial;}' +
7677 '#bbb-dialog-window .bbb-header {border-bottom: 2px solid #CCCCCC; margin-bottom: 5px; margin-right: 100px; padding-right: 50px; white-space: nowrap;}' +
7678 '#bbb-dialog-window .bbb-dialog-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 5px; margin: 0px 5px;}' +
7679 '#bbb-dialog-window .bbb-dialog-content-div {padding: 5px; overflow-x: hidden; overflow-y: auto;}' +
7680 '#bbb-dialog-window .bbb-dialog-button-div {padding-top: 10px; flex-grow: 0; flex-shrink: 0; overflow: hidden;}' +
7681 '#bbb-dialog-window .bbb-edit-area {height: 300px; width: 800px;}' +
7682 '.ui-autocomplete {z-index: 9004 !important;}';
7683
7684 // Provide a little extra space for listings that allow thumbnail_count.
7685 if (thumbnail_count && (gLoc === "search" || gLoc === "favorites")) {
7686 styles += 'div#page {margin: 0px 10px 0px 20px !important;}' +
7687 'section#content {padding: 0px !important;}';
7688 }
7689
7690 // Calculate some dimensions.
7691 var totalBorderWidth = (custom_tag_borders ? border_width * 2 + (border_spacing * 2 || 1) : border_width + border_spacing);
7692 var thumbMaxWidth = 150 + totalBorderWidth * 2;
7693 var thumbMaxHeight = thumbMaxWidth;
7694 var listingExtraSpace = (14 - totalBorderWidth * 2 > 2 ? 14 - totalBorderWidth * 2 : 2);
7695 var commentExtraSpace = 34 - totalBorderWidth * 2;
7696 var customBorderSpacing = (border_spacing || 1);
7697
7698 if (thumb_info === "below")
7699 thumbMaxHeight += 18; // Add some extra height for the info.
7700
7701 // Border setup.
7702 var sbsl = status_borders.length;
7703 var statusBorderItem; // Loop variable.
7704
7705 styles += 'article.post-preview a.bbb-thumb-link, .post-preview div.preview a.bbb-thumb-link {display: inline-block !important;}' +
7706 'article.post-preview {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin: 0px ' + listingExtraSpace + 'px ' + listingExtraSpace + 'px 0px !important;}' +
7707 'article.post-preview.pooled {height: ' + (thumbMaxHeight + 60) + 'px !important;}' + // Pool gallery view thumb height adjustment.
7708 '#has-parent-relationship-preview article.post-preview, #has-children-relationship-preview article.post-preview {padding: 5px 5px 10px !important; width: auto !important; max-width: ' + thumbMaxWidth + 'px !important; margin: 0px !important;}' +
7709 'article.post-preview a.bbb-thumb-link {line-height: 0px !important;}' +
7710 '.post-preview div.preview {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin-right: ' + commentExtraSpace + 'px !important;}' +
7711 '.post-preview div.preview a.bbb-thumb-link {line-height: 0px !important;}' +
7712 '.post-preview a.bbb-thumb-link img {border-width: ' + border_width + 'px !important; padding: ' + border_spacing + 'px !important;}' +
7713 'a.bbb-thumb-link.bbb-custom-tag {border-width: ' + border_width + 'px !important;}';
7714
7715 if (custom_status_borders) {
7716 var activeStatusStyles = "";
7717 var statusBorderInfo = {};
7718
7719 for (i = 0; i < sbsl; i++) {
7720 statusBorderItem = status_borders[i];
7721 statusBorderInfo[statusBorderItem.tags] = statusBorderItem;
7722 }
7723
7724 for (i = 0; i < sbsl; i++) {
7725 statusBorderItem = status_borders[i];
7726
7727 if (single_color_borders) {
7728 if (statusBorderItem.is_enabled)
7729 activeStatusStyles = '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}' + activeStatusStyles;
7730 else
7731 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: transparent !important;}'; // Disable status border by resetting it to transparent.
7732 }
7733 else {
7734 if (statusBorderItem.is_enabled) {
7735 if (statusBorderItem.tags === "parent") {
7736 styles += '.post-preview.post-status-has-children a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}'; // Parent only status border.
7737
7738 if (statusBorderInfo.child.is_enabled)
7739 styles += '.post-preview.post-status-has-children.post-status-has-parent a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' ' + statusBorderInfo.child.border_color + ' ' + statusBorderInfo.child.border_color + ' ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' ' + statusBorderInfo.child.border_style + ' ' + statusBorderInfo.child.border_style + ' ' + statusBorderItem.border_style + ' !important;}'; // Parent and child status border.
7740 }
7741 else if (statusBorderItem.tags === "child")
7742 styles += '.post-preview.post-status-has-parent a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}'; // Child only status border.
7743 else {
7744 activeStatusStyles = '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged only status border.
7745
7746 if (statusBorderInfo.parent.is_enabled)
7747 activeStatusStyles = '.post-preview.post-status-has-children.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderInfo.parent.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderInfo.parent.border_color + ' !important; border-style: ' + statusBorderInfo.parent.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderInfo.parent.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged and parent status border.
7748
7749 if (statusBorderInfo.child.is_enabled)
7750 activeStatusStyles = '.post-preview.post-status-has-parent.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderInfo.child.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderInfo.child.border_color + ' !important; border-style: ' + statusBorderInfo.child.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderInfo.child.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged and child status border.
7751
7752 if (statusBorderInfo.child.is_enabled && statusBorderInfo.parent.is_enabled)
7753 activeStatusStyles = '.post-preview.post-status-has-children.post-status-has-parent.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderInfo.parent.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderInfo.child.border_color + ' !important; border-style: ' + statusBorderInfo.parent.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderInfo.child.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged, parent, and child status border.
7754 }
7755 }
7756 else
7757 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: transparent !important;}'; // Disable status border by resetting it to transparent.
7758 }
7759 }
7760
7761 styles += activeStatusStyles;
7762 }
7763 else if (single_color_borders) { // Allow single color borders when not using custom status borders. Works off of the old border hierarchy: Deleted > Flagged > Pending > Child > Parent
7764 var defaultStatusBorders = bbb.options.status_borders;
7765
7766 for (i = defaultStatusBorders.length - 1; i >= 0; i--) {
7767 statusBorderItem = defaultStatusBorders[i];
7768
7769 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}';
7770 }
7771 }
7772
7773 if (custom_tag_borders) {
7774 styles += '.post-preview a.bbb-thumb-link.bbb-custom-tag img {border-width: 0px !important;}' + // Remove the transparent border for images that get custom tag borders.
7775 'article.post-preview a.bbb-thumb-link, .post-preview div.preview a.bbb-thumb-link {margin-top: ' + (border_width + customBorderSpacing) + 'px !important;}'; // Align one border images with two border images.
7776
7777 for (i = 0; i < sbsl; i++) {
7778 statusBorderItem = status_borders[i];
7779
7780 if (statusBorderItem.is_enabled)
7781 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag {margin: 0px !important; padding: ' + customBorderSpacing + 'px !important;}' + // Remove margin alignment and add border padding for images that have status and custom tag borders.
7782 '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag img {border-width: ' + border_width + 'px !important;}'; // Override the removal of the transparent border for images that have status borders and custom tag borders.
7783 }
7784 }
7785
7786 // Overlay setup.
7787 styles += 'article.post-preview:before, div.post.post-preview div.preview:before {content: none !important;}' + // Disable original Danbooru animated overlay.
7788 'article.post-preview[data-tags~="animated"] a.bbb-thumb-link:before, article.post-preview[data-file-ext="swf"] a.bbb-thumb-link:before, article.post-preview[data-file-ext="webm"] a.bbb-thumb-link:before, article.post-preview[data-file-ext="mp4"] a.bbb-thumb-link:before, div.post.post-preview[data-tags~="animated"] div.preview a.bbb-thumb-link:before, div.post.post-preview[data-file-ext="swf"] div.preview a.bbb-thumb-link:before, div.post.post-preview[data-file-ext="webm"] div.preview a.bbb-thumb-link:before, div.post.post-preview[data-file-ext="mp4"] div.preview a.bbb-thumb-link:before {content: "\\25BA"; position: absolute; width: 20px; height: 20px; color: #FFFFFF; background-color: rgba(0, 0, 0, 0.5); line-height: 20px; top: 0px; left: 0px;}' + // Recreate Danbooru animated overlay.
7789 'article.post-preview[data-has-sound="true"] a.bbb-thumb-link:before, div.post.post-preview[data-has-sound="true"] div.preview a.bbb-thumb-link:before {content: "\\266A"; position: absolute; width: 20px; height: 20px; color: #FFFFFF; background-color: rgba(0, 0, 0, 0.5); line-height: 20px; top: 0px; left: 0px;}' + // Recreate Danbooru audio overlay.
7790 'article.post-preview.blacklisted a.bbb-thumb-link:after, article.post-preview a.bbb-thumb-link:before, div.post.post-preview.blacklisted div.preview a.bbb-thumb-link:after, div.post.post-preview div.preview a.bbb-thumb-link:before {margin: ' + (border_width + border_spacing) + 'px;}' + // Margin applies to posts with no borders or only a status border.
7791 'article.post-preview.blacklisted a.bbb-thumb-link.bbb-custom-tag:after, article.post-preview a.bbb-thumb-link.bbb-custom-tag:before, div.post.post-preview.blacklisted div.preview a.bbb-thumb-link.bbb-custom-tag:after, div.post.post-preview div.preview a.bbb-thumb-link.bbb-custom-tag:before {margin: ' + border_spacing + 'px;}' + // Margin applies to posts with only a custom border.
7792 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link:after, article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link:before, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link:after, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link:before {content: none;}' + // Don't display when actively blacklisted.
7793 'article.post-preview a.bbb-thumb-link, div.post.post-preview div.preview a.bbb-thumb-link {position: relative;}'; // Allow the overlays to position relative to the link.
7794
7795 for (i = 0; i < sbsl; i++) {
7796 statusBorderItem = status_borders[i];
7797
7798 if (statusBorderItem.is_enabled)
7799 styles += 'article.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag:after, article.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag:before, div.post.post-preview.' + statusBorderItem.class_name + ' div.preview a.bbb-thumb-link.bbb-custom-tag:after, div.post.post-preview.' + statusBorderItem.class_name + ' div.preview a.bbb-thumb-link.bbb-custom-tag:before {margin: ' + (border_width + border_spacing + customBorderSpacing) + 'px !important}'; // Margin applies to posts with a status and custom border.
7800 }
7801
7802 // Thumbnail info.
7803 var thumbInfoStyle = "height: 18px; font-size: 14px; line-height: 18px; text-align: center;";
7804
7805 if (thumb_info !== "disabled")
7806 styles += '.bbb-thumb-info-parent.blacklisted.blacklisted-active .bbb-thumb-info {display: none;}';
7807
7808 if (thumb_info === "below")
7809 styles += '.bbb-thumb-info-parent .bbb-thumb-info {display: block;' + thumbInfoStyle + '}';
7810 else if (thumb_info === "hover") {
7811 styles += '.bbb-thumb-info-parent .bbb-thumb-info {display: none; position: relative; bottom: 18px; background-color: rgba(255, 255, 255, 0.9);' + thumbInfoStyle + '}' +
7812 '.bbb-thumb-info-parent:hover .bbb-thumb-info {display: block;}' +
7813 '#has-children-relationship-preview article.post-preview.bbb-thumb-info-parent, #has-parent-relationship-preview article.post-preview.bbb-thumb-info-parent {min-width: 130px !important;}' + // Give parent/child notice thumbs a minimum width to prevent element shifting upon hover.
7814 '.bbb-thumb-info-parent:hover .bbb-thumb-info.bbb-thumb-info-short {bottom: 0px;}'; // Short thumbnails get no overlapping.
7815 }
7816
7817 // Endless
7818 if (endless_default !== "disabled") {
7819 styles += 'div.paginator {padding: 3em 0px 0px;}' +
7820 '#bbb-endless-button-div {width: 100%; height: 0px; overflow: visible; clear: both; text-align: center;}' +
7821 '#bbb-endless-load-div, #bbb-endless-enable-div {display: none; position: absolute;}' +
7822 '#bbb-endless-load-button, #bbb-endless-enable-button {position: relative; left: -50%; border: 1px solid #EAEAEA; border-radius: 5px; display: inline-block; padding: 5px; margin-top: 3px;}';
7823
7824 if (endless_separator === "divider") {
7825 styles += '.bbb-endless-page {display: block; clear: both;}' +
7826 '.bbb-endless-divider {display: block; border: 1px solid #CCCCCC; height: 0px; margin: 15px 0px; width: 100%; float: left;}' +
7827 '.bbb-endless-divider-link {position: relative; top: -16px; display: inline-block; height: 32px; margin-left: 5%; padding: 0px 5px; font-size: 14px; font-weight: bold; line-height: 32px; background-color: #FFFFFF; color: #CCCCCC;}';
7828 }
7829 else if (endless_separator === "marker") {
7830 styles += '.bbb-endless-page {display: inline;}' +
7831 'article.bbb-endless-marker-article {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin: 0px ' + listingExtraSpace + 'px ' + listingExtraSpace + 'px 0px !important; float: left; overflow: hidden; text-align: center; vertical-align: baseline; position: relative;}' +
7832 '.bbb-endless-marker {display: inline-block; border: 1px solid #CCCCCC; height: 148px; width: 148px; line-height: 148px; text-align: center; margin-top: ' + totalBorderWidth + 'px;}' +
7833 '.bbb-endless-marker-link {display: inline-block; font-size: 14px; font-weight: bold; line-height: 14px; vertical-align: middle; color: #CCCCCC;}';
7834 }
7835 else if (endless_separator === "none")
7836 styles += '.bbb-endless-page {display: inline;}';
7837 }
7838
7839 // Hide sidebar.
7840 if (autohide_sidebar) {
7841 styles += 'div#page {margin: 0px 10px 0px 20px !important;}' +
7842 'aside#sidebar {background-color: transparent !important; border-width: 0px !important; height: 100% !important; width: 250px !important; position: fixed !important; left: -285px !important; opacity: 0 !important; overflow: hidden !important; padding: 0px 25px !important; top: 0px !important; z-index: 2001 !important; word-wrap: break-word !important;}' +
7843 'aside#sidebar.bbb-sidebar-show, aside#sidebar:hover {background-color: #FFFFFF !important; border-right: 1px solid #CCCCCC !important; left: 0px !important; opacity: 1 !important; overflow-y: auto !important; padding: 0px 15px !important;}' +
7844 'section#content {margin-left: 0px !important;}';
7845 }
7846
7847 // Tweak the "+" buttom and tags input so they work with the autohide and fixed sidebar options.
7848 if (autohide_sidebar || fixed_sidebar) {
7849 styles += '#search-dropdown {position: absolute !important; top: auto !important; bottom: auto !important; left: auto !important; right: auto !important;}' +
7850 'aside#sidebar input#tags {max-width: ' + (autohide_sidebar ? "240" : document.getElementById("search-box").clientWidth - 10) + 'px;}';
7851 }
7852
7853 // Collapse sidebar sections.
7854 if (collapse_sidebar) {
7855 styles += '#sidebar ul.bbb-collapsed-sidebar, #sidebar form.bbb-collapsed-sidebar {display: block !important; height: 0px !important; margin: 0px !important; padding: 0px !important; overflow: hidden !important;}' + // Hide the element without changing the display to "none" since that interferes with some of Danbooru's JS.
7856 '#sidebar h1, #sidebar h2 {display: inline-block !important;}'; // Inline-block is possible here due to not using display in the previous rule.
7857 }
7858
7859 // Additional blacklist bars.
7860 if (blacklist_add_bars) {
7861 styles += '#blacklist-box.bbb-blacklist-box {margin-bottom: 1em;}' +
7862 '#blacklist-box.bbb-blacklist-box ul {display: inline;}' +
7863 '#blacklist-box.bbb-blacklist-box li {display: inline; margin-right: 1em;}' +
7864 '#blacklist-box.bbb-blacklist-box li a, #blacklist-box.bbb-blacklist-box li span.link {color: #0073FF; cursor: pointer;}' +
7865 '#blacklist-box.bbb-blacklist-box li span {color: #AAAAAA;}';
7866 }
7867
7868 // Blacklist thumbnail display.
7869 if (blacklist_post_display === "removed") {
7870 styles += 'article.post-preview.blacklisted {display: inline-block !important;}' +
7871 'article.post-preview.blacklisted.blacklisted-active {display: none !important;}' +
7872 'div.post.post-preview.blacklisted {display: block !important;}' + // Comment listing override.
7873 'div.post.post-preview.blacklisted.blacklisted-active {display: none !important;}';
7874 }
7875 else if (blacklist_post_display === "hidden") {
7876 styles += 'article.post-preview.blacklisted.blacklisted-active {display: inline-block !important;}' +
7877 'div.post.post-preview.blacklisted {display: block !important;}' + // Comments.
7878 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link {visibility: hidden !important;}';
7879 }
7880 else if (blacklist_post_display === "replaced") {
7881 styles += 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active {display: inline-block !important; background-position: ' + totalBorderWidth + 'px ' + totalBorderWidth + 'px !important; background-repeat: no-repeat !important; background-image: url(' + bbbBlacklistImg + ') !important;}' +
7882 '#has-parent-relationship-preview article.post-preview.blacklisted.blacklisted-active, #has-children-relationship-preview article.post-preview.blacklisted.blacklisted-active {background-position: ' + (totalBorderWidth + 5) + 'px ' + (totalBorderWidth + 5) + 'px !important;}' + // Account for relation notice padding.
7883 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link img, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link img {opacity: 0.0 !important; height: 150px !important; width: 150px !important; border-width: 0px !important; padding: 0px !important;}' + // Remove all status border space.
7884 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link {padding: 0px !important; margin: ' + totalBorderWidth + 'px !important; margin-bottom: 0px !important;}' + // Align no border thumbs with custom/single border thumbs.
7885 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link.bbb-custom-tag, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link.bbb-custom-tag {padding: ' + border_spacing + 'px !important; margin: ' + (border_width + customBorderSpacing) + 'px !important; margin-bottom: 0px !important;}' +
7886 'div.post.post-preview.blacklisted {display: block !important;}' +
7887 'div.post.post-preview.blacklisted.blacklisted-active {display: block !important;}';
7888 }
7889
7890 // Blacklist marking.
7891 if (blacklist_thumb_mark === "icon") {
7892 styles += 'article.post-preview.blacklisted a.bbb-thumb-link:after, div.post.post-preview.blacklisted div.preview a.bbb-thumb-link:after {content: "\\A0"; position: absolute; bottom: 0px; right: 0px; height: 20px; width: 20px; line-height: 20px; font-weight: bold; color: #FFFFFF; background: rgba(0, 0, 0, 0.5) url(\'' + bbbBlacklistIcon + '\');}'; // Create blacklist overlay.
7893 }
7894 else if (blacklist_thumb_mark === "highlight") {
7895 styles += 'article.post-preview.blacklisted, div.post.post-preview.blacklisted div.preview {background-color: ' + blacklist_highlight_color + ' !important;}' +
7896 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active div.preview {background-color: transparent !important;}' +
7897 'article.post-preview.blacklisted.blacklisted-active.current-post {background-color: rgba(0, 0, 0, 0.1) !important}';
7898 }
7899
7900 // Blacklist post controls.
7901 if (blacklist_thumb_controls) {
7902 styles += '#bbb-blacklist-tip {background-color: #FFFFFF; border: 1px solid #000000; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5); display: none; font-size: 12px; line-height: 14px; padding: 5px; position: absolute; max-width: 420px; width: 420px; overflow: hidden; z-index: 9002;}' +
7903 '#bbb-blacklist-tip * {font-size: 12px; line-height: 14px;}' +
7904 '#bbb-blacklist-tip .blacklisted-active {text-decoration: line-through; font-weight: normal;}' +
7905 '#bbb-blacklist-tip ul {list-style: outside disc none; margin-top: 0px; margin-bottom: 0px; margin-left: 15px;}' +
7906 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active div.preview, article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link {cursor: help !important;}' +
7907 'article.post-preview.blacklisted.blacklisted-active a, div.post.post-preview.blacklisted.blacklisted-active div.preview a {cursor: pointer !important;}' +
7908 'article.post-preview.blacklisted, div.post.post-preview.blacklisted div.preview {position: relative !important;}' +
7909 'article.post-preview.blacklisted:hover .bbb-close-circle, div.post.post-preview.blacklisted:hover div.preview .bbb-close-circle {display: block; position: absolute; top: 0px; right: 0px; z-index: 9002 ; cursor: pointer; background-image: url(\'/images/ui-icons_222222_256x240.png\'); background-repeat: no-repeat; background-color: #FFFFFF; background-position: -32px -192px; width: 16px; height: 16px; border-radius: 8px; overflow: hidden;}' +
7910 'article.post-preview.blacklisted.blacklisted-active:hover .bbb-close-circle, div.post.post-preview.blacklisted.blacklisted-active:hover div.preview .bbb-close-circle {display: none;}' +
7911 'article.post-preview.blacklisted .bbb-close-circle, div.post.post-preview.blacklisted div.preview .bbb-close-circle {display: none;}';
7912 }
7913
7914 // Quick search styles.
7915 if (quick_search !== "disabled") {
7916 styles += '#bbb-quick-search {position: fixed; top: 0px; right: 0px; z-index: 2001; overflow: auto; padding: 2px; background-color: #FFFFFF; border-bottom: 1px solid #CCCCCC; border-left: 1px solid #CCCCCC; border-bottom-left-radius: 10px;}' +
7917 '#bbb-quick-search-form {display: none;}' +
7918 '.bbb-quick-search-show #bbb-quick-search-form {display: inline;}' +
7919 '#bbb-quick-search-status, #bbb-quick-search-pin, #bbb-quick-search-negate {border: none; width: 17px; height: 17px; background-color: transparent; background-repeat: no-repeat; background-color: transparent; background-image: url(\'/images/ui-icons_222222_256x240.png\');}' +
7920 '#bbb-quick-search-status {background-position: -160px -112px;}' + // Magnifying glass.
7921 '.bbb-quick-search-active #bbb-quick-search-status, .bbb-quick-search-show.bbb-quick-search-active.bbb-quick-search-pinned #bbb-quick-search-status {background-position: -128px -112px;}' + // Plus magnifying glass.
7922 '#bbb-quick-search-pin {background-position: -128px -144px;}' + // Horizontal pin.
7923 '.bbb-quick-search-pinned #bbb-quick-search-pin, .bbb-quick-search-active.bbb-quick-search-pinned #bbb-quick-search-status {background-position: -144px -144px;}' + // Vertical pin.
7924 '#bbb-quick-search-negate {background-position: -48px -129px;}' + // Negative sign.
7925 '.bbb-quick-search-negated #bbb-quick-search-negate {background-position: -15px -192px;}' + // Negative sign in a dark circle.
7926 '#bbb-quick-search.bbb-quick-search-negated {filter: invert(0.75);}' +
7927 '#bbb-quick-search.bbb-quick-search-negated.bbb-quick-search-show {filter: invert(0);}' +
7928 '#bbb-quick-search.bbb-quick-search-active {background-color: #DDDDDD;}' +
7929 '#bbb-quick-search.bbb-quick-search-active.bbb-quick-search-show {background-color: #FFFFFF;}' +
7930 '#bbb-quick-search-pin:focus, #bbb-quick-search-pin:hover, #bbb-quick-search-negate:focus, #bbb-quick-search-negate:hover {background-color: #CCCCCC;}' +
7931 '#news-updates {padding-right: 25px !important;}';
7932
7933 if (quick_search.indexOf("remove") > -1)
7934 styles += 'article.post-preview.bbb-quick-search-filtered, article.post.post-preview.blacklisted.bbb-quick-search-filtered, article.post-preview.blacklisted.blacklisted-active.bbb-quick-search-filtered {display: none !important;}';
7935 else if (quick_search.indexOf("fade") > -1)
7936 styles += 'article.post-preview.bbb-quick-search-filtered {opacity: 0.1;}';
7937 }
7938
7939 if (resize_link_style === "minimal")
7940 styles += '.bbb-resize-link {display: inline-block; text-align: center; margin-right: 2px; font-size: 87.5%;}';
7941
7942 if (search_add === "remove")
7943 styles += '.search-inc-tag, .search-exl-tag {display: none !important;}';
7944
7945 if (direct_downloads)
7946 styles += '.bbb-ddl {display: none !important;}';
7947
7948 if (post_tag_scrollbars)
7949 styles += '#tag-list ul {max-height: ' + post_tag_scrollbars + 'px !important; overflow-y: auto !important; font-size: 87.5% !important; word-wrap: break-word !important;}';
7950
7951 if (search_tag_scrollbars)
7952 styles += '#tag-box ul {max-height: ' + search_tag_scrollbars + 'px !important; overflow-y: auto !important; font-size: 87.5% !important; margin-right: 2px !important; word-wrap: break-word !important;}';
7953
7954 if (hide_tos_notice && document.getElementById("tos-notice")) {
7955 styles += '#tos-notice {display: none !important;}';
7956
7957 if (manage_cookies)
7958 createCookie("accepted_tos", 1, 365);
7959 }
7960
7961 if (hide_sign_up_notice && document.getElementById("sign-up-notice")) {
7962 styles += '#sign-up-notice {display: none !important;}';
7963
7964 if (manage_cookies)
7965 createCookie("hide_sign_up_notice", 1, 7);
7966 }
7967
7968 if (hide_upgrade_notice && document.getElementById("upgrade-account-notice")) {
7969 styles += '#upgrade-account-notice {display: none !important;}';
7970
7971 if (manage_cookies)
7972 createCookie("hide_upgrade_account_notice", 1, 7);
7973 }
7974
7975 if (hide_ban_notice)
7976 styles += '#ban-notice {display: none !important;}';
7977
7978 if (hide_comment_notice) {
7979 var commentGuide, commentGuideParent; // If/else variables.
7980
7981 if (gLoc === "post") {
7982 commentGuide = document.querySelector("#comments h2 a[href*='howto']");
7983 commentGuideParent = (commentGuide ? commentGuide.parentNode : undefined);
7984
7985 if (commentGuideParent && commentGuideParent.textContent === "Before commenting, read the how to comment guide.")
7986 commentGuideParent.style.display = "none";
7987 }
7988 else if (gLoc === "comments") {
7989 commentGuide = document.querySelector("#a-index div h2 a[href*='howto']");
7990 commentGuideParent = (commentGuide ? commentGuide.parentNode : undefined);
7991
7992 if (commentGuideParent && commentGuideParent.textContent === "Before commenting, read the how to comment guide.")
7993 commentGuideParent.style.display = "none";
7994 }
7995 }
7996
7997 if (hide_tag_notice && gLoc === "post") {
7998 var tagGuide = document.querySelector("#edit div p a[href*='howto']");
7999 var tagGuideParent = (tagGuide ? tagGuide.parentNode : undefined);
8000
8001 if (tagGuideParent && tagGuideParent.textContent === "Before editing, read the how to tag guide.")
8002 tagGuideParent.style.display = "none";
8003 }
8004
8005 if (hide_upload_notice && gLoc === "upload")
8006 styles += '#upload-guide-notice {display: none !important;}';
8007
8008 if (hide_pool_notice && gLoc === "new_pool") {
8009 var poolGuide = document.querySelector("#c-new p a[href*='howto']");
8010 var poolGuideParent = (poolGuide ? poolGuide.parentNode : undefined);
8011
8012 if (poolGuideParent && poolGuideParent.textContent === "Before creating a pool, read the pool guidelines.")
8013 poolGuideParent.style.display = "none";
8014 }
8015
8016 if (hide_hidden_notice)
8017 styles += '.hidden-posts-notice {display: none !important;}';
8018
8019 if (hide_fav_button)
8020 styles += '.fav-buttons {display: none !important;}';
8021
8022 customStyles.innerHTML = styles;
8023 document.getElementsByTagName("head")[0].appendChild(customStyles);
8024 }
8025
8026 function pageCounter() {
8027 // Set up the page counter.
8028 var pageDiv = document.getElementById("page");
8029 var paginator = getPaginator();
8030
8031 if (!page_counter || !paginator || !pageDiv)
8032 return;
8033
8034 var numString = "";
8035 var lastNumString; // If/else variable.
8036
8037 // Provide page number info if available.
8038 if (/\d/.test(paginator.textContent)) {
8039 var activePage = paginator.getElementsByClassName("current-page")[0];
8040 var pageItems = paginator.getElementsByTagName("li");
8041 var numPageItems = pageItems.length;
8042 var lastPageItem = pageItems[numPageItems - 1];
8043 var activeNum = activePage.textContent.bbbSpaceClean();
8044 var lastNum; // If/else variable.
8045
8046 if (activePage.parentNode === lastPageItem) // Last/only page case.
8047 lastNum = activeNum;
8048 else { // In all other cases, there should always be a next page button and at least two other page items (1-X).
8049 lastNum = lastPageItem.previousElementSibling.textContent.bbbSpaceClean();
8050
8051 if (!bbbIsNum(lastNum)) // Too many pages for the current user to view.
8052 lastNum = "";
8053 }
8054
8055 lastNumString = (lastNum ? " of " + lastNum : "");
8056 numString = 'Page ' + activeNum + '<span id="bbb-page-counter-last">' + lastNumString + '</span> | ';
8057 }
8058
8059 var pageNav = bbb.el.pageCounter;
8060 var pageInput = bbb.el.pageCounterInput;
8061
8062 if (!pageNav) { // Create the page nav.
8063 var pageNavString = '<div id="bbb-page-counter" style="float: right; font-size: 87.5%;">' + numString + '<form id="bbb-page-counter-form" style="display: inline;"><input id="bbb-page-counter-input" size="4" placeholder="Page#" type="text"> <input type="submit" value="Go"></form></div>';
8064
8065 pageNav = bbb.el.pageCounter = document.createElement("div");
8066 pageNav.innerHTML = pageNavString;
8067
8068 pageInput = bbb.el.pageCounterInput = getId("bbb-page-counter-input", pageNav);
8069
8070 getId("bbb-page-counter-form", pageNav).addEventListener("submit", function(event) {
8071 var value = pageInput.value.bbbSpaceClean();
8072
8073 if (value !== "")
8074 location.href = updateURLQuery(location.href, {page:value});
8075
8076 event.preventDefault();
8077 }, false);
8078
8079 if (numString)
8080 paginator.bbbWatchNodes(pageCounter);
8081
8082 pageDiv.insertBefore(pageNav, pageDiv.firstElementChild);
8083 }
8084 else // Update the last page in the page nav.
8085 document.getElementById("bbb-page-counter-last").innerHTML = lastNumString;
8086 }
8087
8088 function quickSearch() {
8089 // Set up quick search.
8090 removeInheritedStorage("bbb_quick_search");
8091
8092 if (quick_search === "disabled" || (gLoc !== "search" && gLoc !== "favorites" && gLoc !== "pool" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorite_group"))
8093 return;
8094
8095 var allowAutocomplete = (getMeta("enable-auto-complete") === "true");
8096
8097 // Create the quick search.
8098 var searchDiv = bbb.el.quickSearchDiv = document.createElement("div");
8099 searchDiv.id = "bbb-quick-search";
8100 searchDiv.innerHTML = '<input id="bbb-quick-search-status" type="button" value=""><form id="bbb-quick-search-form"><input id="bbb-quick-search-input" size="75" placeholder="Tags" autocomplete="' + (allowAutocomplete ? "off" : "on") + '" type="text"> <input id="bbb-quick-search-pin" type="button" value=""> <input id="bbb-quick-search-negate" type="button" value=""> <input id="bbb-quick-search-submit" type="submit" value="Go"></form>';
8101
8102 var searchForm = bbb.el.quickSearchForm = getId("bbb-quick-search-form", searchDiv);
8103 var searchInput = bbb.el.quickSearchInput = getId("bbb-quick-search-input", searchDiv);
8104 var searchPin = bbb.el.quickSearchPin = getId("bbb-quick-search-pin", searchDiv);
8105 var searchNegate = bbb.el.quickSearchNegate = getId("bbb-quick-search-negate", searchDiv);
8106 var searchSubmit = bbb.el.quickSearchSubmit = getId("bbb-quick-search-submit", searchDiv);
8107 var searchStatus = bbb.el.quickSearchStatus = getId("bbb-quick-search-status", searchDiv);
8108
8109 // Make the submit event search posts or reset the search.
8110 searchForm.addEventListener("submit", function(event) {
8111 quickSearchToggle();
8112
8113 // Make autocomplete close without getting too tricky.
8114 searchSubmit.focus();
8115 delayMe(function() { searchInput.focus(); }); // Delay this so the blur event has time to register properly.
8116
8117 event.preventDefault();
8118 }, false);
8119
8120 // Hide the search div if the new focus isn't one of the inputs.
8121 searchDiv.addEventListener("blur", function(event) {
8122 var target = event.target;
8123
8124 delayMe(function() {
8125 var active = document.activeElement;
8126
8127 if (active === target || (active !== searchInput && active !== searchSubmit && active !== searchStatus && active !== searchPin && active !== searchNegate))
8128 searchDiv.bbbRemoveClass("bbb-quick-search-show");
8129 });
8130 }, true);
8131
8132 // If a mouse click misses an input within the quick search div, cancel it so the quick search doesn't minimize.
8133 searchDiv.addEventListener("mousedown", function(event) {
8134 var target = event.target;
8135
8136 if (target === searchDiv || target === searchForm)
8137 event.preventDefault();
8138 }, false);
8139
8140 // Hide the search div if the escape key is pressed while using it and autocomplete isn't open.
8141 searchDiv.addEventListener("keydown", function(event) {
8142 if (event.keyCode === 27) {
8143 var jQueryMenu = (searchInput.bbbHasClass("ui-autocomplete-input") ? $("#bbb-quick-search-input").autocomplete("widget")[0] : undefined);
8144
8145 if (jQueryMenu && jQueryMenu.style.display !== "none")
8146 return;
8147
8148 document.activeElement.blur();
8149 event.preventDefault();
8150 }
8151 }, true);
8152
8153 // Show/hide the search div via a left click on the main status icon. If the shift key is held down, toggle the pinned status.
8154 searchStatus.addEventListener("click", function(event) {
8155 if (event.button === 0) {
8156 if (event.shiftKey)
8157 quickSearchPinToggle();
8158 else if (event.ctrlKey) {
8159 quickSearchNegateToggle();
8160 quickSearchToggle();
8161 }
8162 else if (!searchDiv.bbbHasClass("bbb-quick-search-show"))
8163 quickSearchOpen();
8164 else
8165 searchDiv.bbbRemoveClass("bbb-quick-search-show");
8166 }
8167
8168 event.preventDefault();
8169 }, false);
8170
8171 // Reset via a right click on the main status icon.
8172 searchStatus.addEventListener("mouseup", function(event) {
8173 if (event.button === 2 && searchDiv.bbbHasClass("bbb-quick-search-active"))
8174 quickSearchReset();
8175
8176 event.preventDefault();
8177 }, false);
8178
8179 // Stop the context menu on the status icon.
8180 searchStatus.addEventListener("contextmenu", disableEvent, false);
8181
8182 // Make the pin input toggle the pinned status.
8183 searchPin.addEventListener("click", function(event) {
8184 if (event.button === 0)
8185 quickSearchPinToggle();
8186 }, false);
8187
8188 // Make the negate input toggle the negated status.
8189 searchNegate.addEventListener("click", function(event) {
8190 if (event.button === 0)
8191 quickSearchNegateToggle();
8192 }, false);
8193
8194 // Watch the input value and adjust the quick search as needed.
8195 searchInput.addEventListener("input", quickSearchCheck, false);
8196 searchInput.addEventListener("keyup", quickSearchCheck, false);
8197 searchInput.addEventListener("cut", quickSearchCheck, false);
8198 searchInput.addEventListener("paste", quickSearchCheck, false);
8199 searchInput.addEventListener("change", quickSearchCheck, false);
8200
8201 document.body.insertBefore(searchDiv, document.body.firstElementChild);
8202
8203 // Force the submit button to retain its width.
8204 searchDiv.bbbAddClass("bbb-quick-search-show");
8205 searchSubmit.style.width = searchSubmit.offsetWidth + "px";
8206 searchDiv.bbbRemoveClass("bbb-quick-search-show");
8207
8208 // Set up autocomplete.
8209 otherAutocomplete(searchInput);
8210
8211 // Check if the quick search has been pinned for this session.
8212 var pinnedSearch = parseJson(sessionStorage.getItem("bbb_quick_search"), undefined);
8213
8214 if (pinnedSearch) {
8215 bbb.quick_search = pinnedSearch;
8216 searchInput.value = pinnedSearch.tags;
8217 searchDiv.bbbAddClass("bbb-quick-search-pinned" + (pinnedSearch.negated ? " bbb-quick-search-negated" : ""));
8218 quickSearchTest();
8219 }
8220
8221 // Create the hotkeys.
8222 createHotkey("70", quickSearchOpen); // F
8223 createHotkey("s70", quickSearchReset); // SHIFT + F
8224 }
8225
8226 function quickSearchToggle() {
8227 // Submit the quick search or reset it if the current search is active.
8228 var searchInput = bbb.el.quickSearchInput;
8229 var searchDiv = bbb.el.quickSearchDiv;
8230 var oldValue = bbb.quick_search.tags.bbbSpaceClean();
8231 var oldNegate = bbb.quick_search.negated;
8232 var curValue = searchInput.value.bbbSpaceClean();
8233 var curNegate = searchDiv.bbbHasClass("bbb-quick-search-negated");
8234
8235 if (curValue === "" || (curValue === oldValue && curNegate === oldNegate))
8236 quickSearchReset();
8237 else {
8238 bbb.quick_search.tags = searchInput.value;
8239 bbb.quick_search.negated = searchDiv.bbbHasClass("bbb-quick-search-negated");
8240
8241 if (searchDiv.bbbHasClass("bbb-quick-search-pinned"))
8242 sessionStorage.bbbSetItem("bbb_quick_search", JSON.stringify(bbb.quick_search));
8243 else if (quick_search.indexOf("pinned") > -1)
8244 quickSearchPinEnable();
8245
8246 quickSearchTest();
8247 }
8248 }
8249
8250 function quickSearchCheck() {
8251 // Check the input value and adjust the submit button appearance accordingly.
8252 var input = bbb.el.quickSearchInput;
8253 var submit = bbb.el.quickSearchSubmit;
8254 var oldValue = bbb.quick_search.tags.bbbSpaceClean();
8255 var oldNegate = bbb.quick_search.negated;
8256 var curValue = input.value.bbbSpaceClean();
8257 var curNegate = bbb.el.quickSearchDiv.bbbHasClass("bbb-quick-search-negated");
8258
8259 if (oldValue === curValue && curValue !== "" && oldNegate === curNegate)
8260 submit.value = "X";
8261 else
8262 submit.value = "Go";
8263 }
8264
8265 function quickSearchReset() {
8266 // Completely reset the quick search.
8267 var filteredPosts = document.getElementsByClassName("bbb-quick-search-filtered");
8268 var filteredPost = filteredPosts[0];
8269
8270 bbb.quick_search = {negated: false, tags: ""};
8271 bbb.el.quickSearchInput.value = "";
8272 bbb.el.quickSearchSubmit.value = "Go";
8273 bbb.el.quickSearchStatus.title = "";
8274 sessionStorage.removeItem("bbb_quick_search");
8275 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-active bbb-quick-search-pinned bbb-quick-search-negated");
8276
8277 while (filteredPost) {
8278 filteredPost.bbbRemoveClass("bbb-quick-search-filtered");
8279 enablePostDDL(filteredPost);
8280 filteredPost = filteredPosts[0];
8281 }
8282 }
8283
8284 function quickSearchTest(target) {
8285 // Test posts to see if they match the search.
8286 var value = bbb.quick_search.tags.bbbSpaceClean();
8287
8288 if (value === "")
8289 return;
8290 else if (bbb.quick_search.negated)
8291 value = "-( " + value + " )";
8292
8293 var posts = getPosts(target);
8294 var search = createSearch(value);
8295
8296 bbb.el.quickSearchSubmit.value = "X";
8297 bbb.el.quickSearchStatus.title = value;
8298 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-active");
8299
8300 for (var i = 0, il = posts.length; i < il; i++) {
8301 var post = posts[i];
8302
8303 if (!thumbSearchMatch(post, search)) {
8304 post.bbbAddClass("bbb-quick-search-filtered");
8305 disablePostDDL(post);
8306 }
8307 else {
8308 post.bbbRemoveClass("bbb-quick-search-filtered");
8309 enablePostDDL(post);
8310 }
8311 }
8312 }
8313
8314 function quickSearchOpen() {
8315 // Open the quick search div and place focus on the input.
8316 var searchDiv = bbb.el.quickSearchDiv;
8317 var searchInput = bbb.el.quickSearchInput;
8318
8319 searchInput.value = bbb.quick_search.tags;
8320
8321 if (bbb.quick_search.negated === true)
8322 searchDiv.bbbAddClass("bbb-quick-search-negated");
8323 else
8324 searchDiv.bbbRemoveClass("bbb-quick-search-negated");
8325
8326 quickSearchCheck();
8327 searchDiv.bbbAddClass("bbb-quick-search-show");
8328 searchInput.focus();
8329 }
8330
8331 function quickSearchPinToggle() {
8332 // Toggle the quick search between pinned and not pinned for the session.
8333 var searchDiv = bbb.el.quickSearchDiv;
8334
8335 if (searchDiv.bbbHasClass("bbb-quick-search-show", "bbb-quick-search-active")) {
8336 if (!searchDiv.bbbHasClass("bbb-quick-search-pinned"))
8337 quickSearchPinEnable();
8338 else
8339 quickSearchPinDisable();
8340 }
8341 }
8342
8343 function quickSearchPinEnable() {
8344 // Enable the quick search pin.
8345 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-pinned");
8346
8347 if (bbb.quick_search.tags)
8348 sessionStorage.bbbSetItem("bbb_quick_search", JSON.stringify(bbb.quick_search));
8349 }
8350
8351 function quickSearchPinDisable() {
8352 // Disable the quick search pin.
8353 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-pinned");
8354 sessionStorage.removeItem("bbb_quick_search");
8355 }
8356
8357 function quickSearchNegateToggle() {
8358 // Toggle the quick search between negated and not negated.
8359 var searchDiv = bbb.el.quickSearchDiv;
8360
8361 if (searchDiv.bbbHasClass("bbb-quick-search-show", "bbb-quick-search-active")) {
8362 if (!searchDiv.bbbHasClass("bbb-quick-search-negated"))
8363 quickSearchNegateEnable();
8364 else
8365 quickSearchNegateDisable();
8366 }
8367 }
8368
8369 function quickSearchNegateEnable() {
8370 // Enable the quick search negation.
8371 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-negated");
8372 quickSearchCheck();
8373 }
8374
8375 function quickSearchNegateDisable() {
8376 // Disable the quick search negation.
8377 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-negated");
8378 quickSearchCheck();
8379 }
8380
8381 function commentScoreInit() {
8382 // Set up the initial comment scores and get ready to handle new comments.
8383 if (!comment_score || (gLoc !== "comments" && gLoc !== "comment_search" && gLoc !== "comment" && gLoc !== "post"))
8384 return;
8385
8386 var paginator = getPaginator();
8387 var watchedNode = (paginator ? paginator.parentNode : document.body);
8388
8389 commentScore();
8390 watchedNode.bbbWatchNodes(commentScore);
8391 }
8392
8393 function commentScore() {
8394 // Add score links to comments that link directly to that comment.
8395 var comments = document.getElementsByClassName("comment");
8396 var scoredComments = document.getElementsByClassName("bbb-comment-score");
8397
8398 // If all the comments are scored, just stop.
8399 if (comments.length === scoredComments.length)
8400 return;
8401
8402 for (var i = 0, il = comments.length; i < il; i++) {
8403 var comment = comments[i];
8404
8405 // Skip if the comment is already scored.
8406 if (comment.getElementsByClassName("bbb-comment-score")[0])
8407 continue;
8408
8409 var score = comment.bbbInfo("score");
8410 var commentId = comment.bbbInfo("comment-id");
8411 var content = comment.getElementsByClassName("content")[0];
8412 var menu = (content ? content.getElementsByTagName("menu")[0] : undefined);
8413
8414 if (content && !menu) {
8415 menu = document.createElement("menu");
8416 content.appendChild(menu);
8417 }
8418
8419 var menuItems = menu.getElementsByTagName("li");
8420 var listingItemSibling = menuItems[1];
8421
8422 for (var j = 0, jl = menuItems.length; j < jl; j++) {
8423 var menuItem = menuItems[j];
8424 var nextItem = menuItems[j + 1];
8425
8426 if (menuItem.textContent.indexOf("Reply") > -1) {
8427 if (nextItem)
8428 listingItemSibling = nextItem;
8429 else
8430 listingItemSibling = undefined;
8431 }
8432 }
8433
8434 var scoreItem = document.createElement("li");
8435 scoreItem.className = "bbb-comment-score";
8436
8437 var scoreLink = document.createElement("a");
8438 scoreLink.innerHTML = "Score: " + score;
8439 scoreLink.href = "/comments/" + commentId;
8440 scoreItem.appendChild(scoreLink);
8441
8442 if (listingItemSibling)
8443 menu.insertBefore(scoreItem, listingItemSibling);
8444 else
8445 menu.appendChild(scoreItem);
8446 }
8447 }
8448
8449 function postLinkNewWindow() {
8450 // Make thumbnail clicks open in a new tab/window.
8451 if (post_link_new_window === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "favorites" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorite_group"))
8452 return;
8453
8454 document.addEventListener("click", function(event) {
8455 var bypass = (event.shiftKey && event.ctrlKey);
8456 var modeSection = document.getElementById("mode-box");
8457 var danbMode = getCookie().mode || "view";
8458
8459 if (event.button !== 0 || event.altKey || !bypass && (event.shiftKey || event.ctrlKey) || (modeSection && danbMode !== "view"))
8460 return;
8461
8462 var runEndless = (post_link_new_window.indexOf("endless") > -1);
8463 var runNormal = (post_link_new_window.indexOf("normal") > -1);
8464
8465 if ((bbb.endless.enabled && !runEndless) || (!bbb.endless.enabled && !runNormal))
8466 return;
8467
8468 var target = event.target;
8469 var targetTag = target.tagName;
8470 var url; // If/else variable.
8471
8472 if (targetTag === "IMG" && target.bbbParent("A", 3))
8473 url = target.bbbParent("A", 3).href;
8474 else if (targetTag === "A" && target.bbbHasClass("bbb-post-link", "bbb-thumb-link"))
8475 url = target.href;
8476
8477 if (url && /\/posts\/\d+/.test(url)) {
8478 if (bypass)
8479 location.href = url;
8480 else
8481 window.open(url);
8482
8483 event.preventDefault();
8484 }
8485 }, false);
8486 }
8487
8488 function formatTip(event, el, content, x, y) {
8489 // Position + resize the tip and display it.
8490 var tip = el;
8491 var windowX = event.clientX;
8492 var windowY = event.clientY;
8493 var topOffset = 0;
8494 var leftOffset = 0;
8495
8496 if (typeof(content) === "string")
8497 tip.innerHTML = content;
8498 else {
8499 tip.innerHTML = "";
8500 tip.appendChild(content);
8501 }
8502
8503 tip.style.visibility = "hidden";
8504 tip.style.display = "block";
8505
8506 // Resize the tip to minimize blank space.
8507 var origHeight = tip.clientHeight;
8508 var padding = tip.bbbGetPadding();
8509 var paddingWidth = padding.width;
8510
8511 while (origHeight >= tip.clientHeight && tip.clientWidth > 15)
8512 tip.style.width = tip.clientWidth - paddingWidth - 2 + "px";
8513
8514 tip.style.width = tip.clientWidth - paddingWidth + 2 + "px";
8515
8516 if (tip.scrollWidth > tip.clientWidth)
8517 tip.style.width = "auto";
8518
8519 // Don't allow the tip to go above the top of the window.
8520 if (windowY - tip.offsetHeight < 5)
8521 topOffset = windowY - tip.offsetHeight - 5;
8522
8523 // Don't allow the tip to go beyond the left edge of the window.
8524 if (windowX - tip.offsetWidth < 5)
8525 leftOffset = tip.offsetWidth + 1;
8526
8527 tip.style.left = x - tip.offsetWidth + leftOffset + "px";
8528 tip.style.top = y - tip.offsetHeight - topOffset + "px";
8529 tip.style.visibility = "visible";
8530 }
8531
8532 function bbbHotkeys() {
8533 // Handle keydown events not taking place in text inputs and check if they're a hotkey.
8534 document.addEventListener("keydown", function(event) {
8535 var active = document.activeElement;
8536 var activeTag = active.tagName;
8537 var activeType = active.type;
8538
8539 if (activeTag === "SELECT" || activeTag === "TEXTAREA" || (activeTag === "INPUT" && !/^(?:button|checkbox|file|hidden|image|radio|reset|submit)$/.test(activeType))) // Input types: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes
8540 return;
8541
8542 var loc = (gLoc === "post" ? "post" : "other");
8543 var hotkeyCode = createHotkeyCode(event);
8544 var hotkey = bbb.hotkeys[loc][hotkeyCode];
8545
8546 if (hotkey) {
8547 var customHandler = hotkey.custom_handler;
8548 customHandler = (typeof(customHandler) !== "boolean" || customHandler !== true ? false : true);
8549
8550 hotkey.func(event); // The event object will always be the first argument passed to the provided function (previously declared or anonymous).
8551
8552 if (!customHandler) {
8553 event.preventDefault();
8554 event.stopPropagation();
8555 }
8556 }
8557 }, true);
8558 }
8559
8560 function createHotkeyCode(event) {
8561 // Take a keyboard event and create a code for its key combination.
8562 // Examples: s49 = Shift + "1", a50 = Alt + "2", cs51 = Control + Shift + "3"
8563 // Alt, control, meta, and shift abbreviations should be alphabetical. Keycode numbers: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
8564 var hotkeycode = "";
8565
8566 if (event.altKey)
8567 hotkeycode += "a";
8568
8569 if (event.ctrlKey)
8570 hotkeycode += "c";
8571
8572 if (event.metaKey)
8573 hotkeycode += "m";
8574
8575 if (event.shiftKey)
8576 hotkeycode += "s";
8577
8578 if (event.keyCode)
8579 hotkeycode += event.keyCode;
8580
8581 return (hotkeycode || undefined);
8582 }
8583
8584 function createHotkey(hotkeyCode, func, propObject) {
8585 // Create hotkeys or override Danbooru's existing ones. Creating a hotkey for a hotkey that already exists will replace it.
8586 var loc = (gLoc === "post" ? "post" : "other");
8587 var hotkeyObject = {func: func};
8588
8589 if (propObject) {
8590 for (var i in propObject) {
8591 if (propObject.hasOwnProperty(i))
8592 hotkeyObject[i] = propObject[i];
8593 }
8594 }
8595
8596 bbb.hotkeys[loc][hotkeyCode] = hotkeyObject;
8597 }
8598
8599 function removeHotkey(hotkeyCode) {
8600 // Remove a hotkey.
8601 var loc = (gLoc === "post" ? "post" : "other");
8602
8603 delete bbb.hotkeys[loc][hotkeyCode];
8604 }
8605
8606 function resizeHotkey(event) {
8607 // Handle the resize post hotkeys and make sure they don't interfere with the favorite group dialog box hotkeys.
8608 var favGroup = document.querySelector("div[aria-describedby='add-to-favgroup-dialog']");
8609
8610 if (favGroup && favGroup.style.display !== "none")
8611 return;
8612
8613 var keyCode = event.keyCode;
8614 var mode; // Switch variable.
8615
8616 switch (keyCode) {
8617 case 49:
8618 mode = "all";
8619 break;
8620 case 50:
8621 mode = "width";
8622 break;
8623 case 51:
8624 mode = "height";
8625 break;
8626 case 52:
8627 default:
8628 mode = "none";
8629 break;
8630 }
8631
8632 resizePost(mode);
8633 event.preventDefault();
8634 event.stopPropagation();
8635 }
8636
8637 function fixLimit(limit) {
8638 // Add the limit variable to link URLs that are not thumbnails.
8639 if (!thumbnail_count && limit === undefined)
8640 return;
8641
8642 var newLimit = (limit === undefined ? thumbnail_count : limit) || undefined;
8643 var page = document.getElementById("page");
8644 var header = document.getElementById("top");
8645 var searchParent = document.getElementById("search-box") || document.getElementById("a-intro");
8646 var i, il, links, link, linkHref; // Loop variables.
8647
8648 if (page) {
8649 var linkRegEx = /^\/(?:posts(?:\/\d+)?|favorites)\?/i;
8650 links = page.getElementsByTagName("a");
8651
8652 for (i = 0, il = links.length; i < il; i++) {
8653 link = links[i];
8654 linkHref = link.getAttribute("href"); // Use getAttribute so that we get the exact value. "link.href" adds in the domain.
8655
8656 if (linkHref && linkHref.indexOf("page=") < 0 && linkRegEx.test(linkHref))
8657 link.href = updateURLQuery(linkHref, {limit: newLimit});
8658 }
8659 }
8660
8661 if (header) {
8662 links = header.getElementsByTagName("a");
8663
8664 for (i = 0, il = links.length; i < il; i++) {
8665 link = links[i];
8666 linkHref = link.getAttribute("href");
8667
8668 if (linkHref && (linkHref.indexOf("limit=") > -1 || linkHref.indexOf("/posts") === 0 || linkHref === "/" || linkHref === "/notes?group_by=post" || linkHref === "/favorites"))
8669 link.href = updateURLQuery(linkHref, {limit: newLimit});
8670 }
8671 }
8672
8673 // Fix the search.
8674 if (searchParent && (gLoc === "search" || gLoc === "post" || gLoc === "intro" || gLoc === "favorites")) {
8675 var search = searchParent.getElementsByTagName("form")[0];
8676
8677 if (search) {
8678 var limitInput = bbb.el.limitInput;
8679
8680 if (!limitInput) {
8681 limitInput = bbb.el.limitInput = document.createElement("input");
8682 limitInput.name = "limit";
8683 limitInput.value = newLimit;
8684 limitInput.type = "hidden";
8685 search.appendChild(limitInput);
8686
8687 // Change the form action if on the favorites page. It uses "/favorites", but that just goes to the normal "/posts" search while stripping out the limit.
8688 search.action = "/posts";
8689
8690 // Remove the user's default limit if the user tries to specify a limit value in the tags.
8691 var tagsInput = document.getElementById("tags");
8692
8693 if (tagsInput) {
8694 search.addEventListener("submit", function() {
8695 if (/(?:^|\s)limit:/.test(tagsInput.value))
8696 search.removeChild(limitInput);
8697 else if (limitInput.parentNode !== search)
8698 search.appendChild(limitInput);
8699 }, false);
8700 }
8701 }
8702 else
8703 limitInput.value = newLimit || thumbnail_count_default;
8704 }
8705 }
8706 }
8707
8708 function fixURLLimit() {
8709 // Update the URL limit value with the user's limit.
8710 if (allowUserLimit()) {
8711 var state = history.state;
8712 var url = updateURLQuery(location.search, {limit: thumbnail_count});
8713
8714 history.replaceState(state, "", url);
8715 location.replace(location.href.split("#", 1)[0] + "#"); // Force browser caching to cooperate.
8716 history.replaceState(state, "", url);
8717 }
8718 }
8719
8720 function saveStateCache() {
8721 // Cache a search's thumbnails to history state to prevent replaceState/browser caching issues.
8722 var state = history.state || {};
8723
8724 if (isRandomSearch()) {
8725 var posts = getPosts();
8726 var postsObject = [];
8727
8728 for (var i = 0, il = posts.length; i < il; i++)
8729 postsObject.push(unformatInfo(posts[i].bbbInfo()));
8730
8731 var postsHash = String(JSON.stringify(postsObject).bbbHash());
8732
8733 state.bbb_posts_cache = {hash: postsHash, posts: postsObject};
8734 sessionStorage.bbbSetItem("bbb_posts_cache", postsHash); // Key used to detect if the page is reloaded/re-entered.
8735 history.replaceState(state, "");
8736 }
8737 }
8738
8739 function checkStateCache() {
8740 // Check for the history state cache and erase it if the page is reloaded or re-entered.
8741 removeInheritedStorage("bbb_posts_cache");
8742
8743 var historyHash = sessionStorage.getItem("bbb_posts_cache");
8744 var state = history.state || {};
8745
8746 if (state.bbb_posts_cache) {
8747 var stateHash = state.bbb_posts_cache.hash;
8748
8749 if (historyHash === stateHash) { // Reloaded. Remove everything since we're on the same page.
8750 delete state.bbb_posts_cache;
8751 history.replaceState(state, "");
8752 sessionStorage.removeItem("bbb_posts_cache");
8753 }
8754 else // Returned. Set the hash again since we're back.
8755 sessionStorage.bbbSetItem("bbb_posts_cache", stateHash);
8756 }
8757 else if (historyHash) // Back/forward. Remove the hash since we're on a new page.
8758 sessionStorage.removeItem("bbb_posts_cache");
8759 }
8760
8761 function autohideSidebar() {
8762 // Show the sidebar when it gets focus, hide it when it loses focus, and only allow select elements to retain focus.
8763 var sidebar = document.getElementById("sidebar");
8764
8765 if (!autohide_sidebar || !sidebar)
8766 return;
8767
8768 sidebar.addEventListener("click", function(event) {
8769 var target = event.target;
8770
8771 if (event.button === 0 && target.id !== "tags")
8772 target.blur();
8773 }, false);
8774 sidebar.addEventListener("mouseup", function(event) {
8775 var target = event.target;
8776
8777 if (event.button !== 0 && target.id !== "tags")
8778 target.blur();
8779 }, false);
8780 sidebar.addEventListener("focus", function() {
8781 sidebar.bbbAddClass("bbb-sidebar-show");
8782 }, true);
8783 sidebar.addEventListener("blur", function() {
8784 sidebar.bbbRemoveClass("bbb-sidebar-show");
8785 }, true);
8786 }
8787
8788 function fixedSidebar() {
8789 // Fix the scrollbar to the top/bottom of the window when it would normally scroll out of view.
8790 var sidebar = bbb.fixed_sidebar.sidebar = document.getElementById("sidebar");
8791 var content = bbb.fixed_sidebar.content = document.getElementById("content");
8792 var comments = document.getElementById("comments");
8793
8794 if (!fixed_sidebar || autohide_sidebar || !sidebar || !content || (gLoc === "post" && !comments))
8795 return;
8796
8797 var docRect = document.documentElement.getBoundingClientRect();
8798 var sidebarRect = sidebar.getBoundingClientRect();
8799 var sidebarTop = bbb.fixed_sidebar.top = sidebarRect.top - docRect.top;
8800 var sidebarLeft = bbb.fixed_sidebar.left = sidebarRect.left - docRect.left;
8801 var sidebarHeight = sidebarRect.height;
8802
8803 content.style.minHeight = sidebarHeight - 1 + "px";
8804
8805 // There are some cases where text overflows.
8806 sidebar.style.overflow = "hidden";
8807 sidebar.style.wordWrap = "break-word";
8808
8809 if (comments)
8810 comments.style.overflow = "auto"; // Force the contained float elements to affect the dimensions.
8811
8812 fixedSidebarCheck();
8813 document.body.bbbWatchNodes(fixedSidebarCheck);
8814 document.addEventListener("keyup", fixedSidebarCheck, false);
8815 document.addEventListener("click", fixedSidebarCheck, false);
8816 window.addEventListener("scroll", fixedSidebarCheck, false);
8817 window.addEventListener("resize", fixedSidebarCheck, false);
8818 }
8819
8820 function fixedSidebarCheck() {
8821 // Event handler for adjusting the sidebar position.
8822 var sidebar = bbb.fixed_sidebar.sidebar;
8823 var content = bbb.fixed_sidebar.content;
8824 var sidebarTop = bbb.fixed_sidebar.top;
8825 var sidebarLeft = bbb.fixed_sidebar.left;
8826 var verScrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
8827 var horScrolled = window.payeXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
8828 var sidebarHeight = sidebar.clientHeight; // Height can potentially change (blacklist update, etc.) so always recalculate it.
8829 var contentHeight = content.clientHeight;
8830 var viewHeight = document.documentElement.clientHeight;
8831 var sidebarBottom = sidebarTop + sidebarHeight;
8832 var contentBottom = sidebarTop + contentHeight;
8833 var viewportBottom = verScrolled + viewHeight;
8834
8835 if (sidebarHeight > contentHeight) // Don't fix to window if there's no space for it to scroll.
8836 sidebar.style.position = "static";
8837 else if (sidebarHeight < viewHeight) { // Fix to the top of the window if not too tall and far enough down.
8838 if (contentBottom < verScrolled + sidebarHeight) {
8839 sidebar.style.position = "absolute";
8840 sidebar.style.bottom = viewHeight - contentBottom + "px";
8841 sidebar.style.top = "auto";
8842 }
8843 else if (sidebarTop < verScrolled ) {
8844 sidebar.style.position = "fixed";
8845 sidebar.style.bottom = "auto";
8846 sidebar.style.top = "0px";
8847 }
8848 else
8849 sidebar.style.position = "static";
8850 }
8851 else { // If too tall, fix the sidebar bottom to the viewport bottom to avoid putting part of the sidebar permanently beyond reach.
8852 if (viewportBottom > contentBottom) {
8853 sidebar.style.position = "absolute";
8854 sidebar.style.bottom = viewHeight - contentBottom + "px";
8855 sidebar.style.top = "auto";
8856 }
8857 else if (sidebarTop > verScrolled || sidebarTop > viewportBottom - sidebarHeight)
8858 sidebar.style.position = "static";
8859 else if (sidebarBottom < viewportBottom) {
8860 sidebar.style.position = "fixed";
8861 sidebar.style.bottom = "0px";
8862 sidebar.style.top = "auto";
8863 }
8864 }
8865
8866 // Maintain horizontal position in the document.
8867 if (horScrolled && sidebar.style.position !== "absolute")
8868 sidebar.style.left = (sidebarLeft - horScrolled) + "px";
8869 else
8870 sidebar.style.left = sidebarLeft + "px";
8871 }
8872
8873 function fixedPaginator() {
8874 // Set up the fixed paginator.
8875 if (fixed_paginator === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "favorites" && gLoc !== "favorite_group"))
8876 return;
8877
8878 var paginator = getPaginator();
8879 var paginatorMenu = (paginator ? paginator.getElementsByTagName("menu")[0] : undefined);
8880 var paginatorLink = (paginatorMenu ? (paginatorMenu.getElementsByTagName("a")[0] || paginatorMenu.getElementsByTagName("span")[0]) : undefined);
8881
8882 if (!paginatorLink)
8883 return;
8884
8885 // Get all our measurements.
8886 var docRect = document.documentElement.getBoundingClientRect();
8887 var docWidth = docRect.width;
8888 var docBottom = docRect.bottom;
8889
8890 var paginatorRect = paginator.getBoundingClientRect();
8891 var paginatorBottom = paginatorRect.bottom;
8892 var paginatorLeft = paginatorRect.left;
8893 var paginatorRight = docWidth - paginatorRect.right;
8894 var paginatorHeight = paginatorRect.height;
8895
8896 var menuRect = paginatorMenu.getBoundingClientRect();
8897 var menuBottom = menuRect.bottom;
8898
8899 var linkRect = paginatorLink.getBoundingClientRect();
8900 var linkBottom = linkRect.bottom;
8901
8902 var paginatorMargAdjust = (paginatorLeft - paginatorRight) / 2;
8903 var menuBottomAdjust = linkBottom - menuBottom;
8904
8905 var paginatorSpacer = document.createElement("div"); // Prevents the document height form changing when the paginator is fixed to the bottom of the window.
8906 paginatorSpacer.id = "bbb-fixed-paginator-spacer";
8907
8908 var paginatorSibling = paginator.nextElementSibling;
8909
8910 if (paginatorSibling)
8911 paginator.parentNode.insertBefore(paginatorSpacer, paginatorSibling);
8912 else
8913 paginator.parentNode.appendChild(paginatorSpacer);
8914
8915 // Create the CSS for the fixed paginator separately from the main one since it needs to know what the page's final layout will be with the main CSS applied.
8916 var style = document.createElement("style");
8917 style.type = "text/css";
8918 style.innerHTML = '.bbb-fixed-paginator div.paginator {position: fixed; padding: 0px; margin: 0px; bottom: 0px; left: 50%; margin-left: ' + paginatorMargAdjust + 'px;}' +
8919 '.bbb-fixed-paginator div.paginator menu {position: relative; left: -50%; padding: ' + menuBottomAdjust + 'px 0px; background-color: #FFFFFF;}' +
8920 '.bbb-fixed-paginator div.paginator menu li:first-child {padding-left: 0px;}' +
8921 '.bbb-fixed-paginator div.paginator menu li:first-child > * {margin-left: 0px;}' +
8922 '.bbb-fixed-paginator div.paginator menu li:last-child {padding-right: 0px;}' +
8923 '.bbb-fixed-paginator div.paginator menu li:last-child > * {margin-right: 0px;}' +
8924 '#bbb-fixed-paginator-spacer {display: none; height: ' + paginatorHeight + 'px; clear: both; width: 100%;}' +
8925 '.bbb-fixed-paginator #bbb-fixed-paginator-spacer {display: block;}';
8926
8927 if (fixed_paginator.indexOf("minimal") > -1) {
8928 style.innerHTML += '.bbb-fixed-paginator div.paginator menu {padding: 3px 0px;}' +
8929 '.bbb-fixed-paginator div.paginator menu li a, .bbb-fixed-paginator div.paginator menu li span {padding: 2px; margin: 0px 2px 0px 0px;}' +
8930 '.bbb-fixed-paginator div.paginator menu li {padding: 0px;}' +
8931 '.bbb-fixed-paginator div.paginator menu li a {border-color: #CCCCCC;}';
8932 }
8933
8934 document.getElementsByTagName("head")[0].appendChild(style);
8935
8936 bbb.fixed_paginator_space = docBottom - paginatorBottom - menuBottomAdjust; // Store the amount of space between the bottom of the page and the paginator.
8937
8938 document.body.bbbWatchNodes(fixedPaginatorCheck);
8939 document.addEventListener("keyup", fixedPaginatorCheck, false);
8940 document.addEventListener("click", fixedPaginatorCheck, false);
8941 window.addEventListener("scroll", fixedPaginatorCheck, false);
8942 window.addEventListener("resize", fixedPaginatorCheck, false);
8943
8944 fixedPaginatorCheck();
8945 }
8946
8947 function fixedPaginatorCheck() {
8948 // Check if the paginator needs to be in its default position or fixed to the window.
8949 if (!bbb.fixed_paginator_space)
8950 return;
8951
8952 var runEndless = (fixed_paginator.indexOf("endless") > -1);
8953 var runNormal = (fixed_paginator.indexOf("normal") > -1);
8954 var docHeight = document.documentElement.scrollHeight;
8955 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
8956 var viewHeight = document.documentElement.clientHeight;
8957
8958 if (viewHeight + scrolled < docHeight - bbb.fixed_paginator_space && ((runEndless && bbb.endless.enabled) || (runNormal && !bbb.endless.enabled)))
8959 document.body.bbbAddClass("bbb-fixed-paginator");
8960 else
8961 document.body.bbbRemoveClass("bbb-fixed-paginator");
8962 }
8963
8964 function collapseSidebar() {
8965 // Allow clicking on headers to collapse and expand their respective sections.
8966 var sidebar = document.getElementById("sidebar");
8967
8968 if (!collapse_sidebar || !sidebar)
8969 return;
8970
8971 var dataLoc = (gLoc === "post" ? "post" : "thumb");
8972 var data = collapse_sidebar_data[dataLoc];
8973 var headers = sidebar.querySelectorAll("h1, h2");
8974 var nameList = " ";
8975 var removedOld = false;
8976 var i, il; // Loop variables.
8977
8978 // Grab the desired tags and turn them into toggle elements for their section.
8979 for (i = 0, il = headers.length; i < il; i++) {
8980 var header = headers[i];
8981 var name = header.textContent.bbbSpaceClean().replace(" ", "_");
8982 var collapse = data[name];
8983 var sibling = header.nextElementSibling;
8984 nameList += name + " ";
8985
8986 header.addEventListener("click", collapseSidebarToggle, false);
8987 header.addEventListener("mouseup", collapseSidebarDefaultToggle.bind(null, name), false);
8988 header.addEventListener("contextmenu", disableEvent, false);
8989
8990 if (collapse && sibling)
8991 sibling.bbbAddClass("bbb-collapsed-sidebar");
8992 }
8993
8994 // Clean up potential old section names.
8995 for (i in data) {
8996 if (data.hasOwnProperty(i) && nameList.indexOf(i.bbbSpacePad()) < 0) {
8997 removedOld = true;
8998 delete data[i];
8999 }
9000 }
9001
9002 if (removedOld) {
9003 loadSettings();
9004 bbb.user.collapse_sidebar_data[dataLoc] = data;
9005 saveSettings();
9006 }
9007 }
9008
9009 function collapseSidebarToggle(event) {
9010 // Collapse/expand a sidebar section.
9011 var target = event.target;
9012 var sibling = target.nextElementSibling;
9013
9014 if (event.button !== 0 || !sibling)
9015 return;
9016
9017 if (sibling.bbbHasClass("bbb-collapsed-sidebar"))
9018 sibling.bbbRemoveClass("bbb-collapsed-sidebar");
9019 else
9020 sibling.bbbAddClass("bbb-collapsed-sidebar");
9021
9022 event.preventDefault();
9023 }
9024
9025 function collapseSidebarDefaultToggle(name, event) {
9026 // Make a sidebar section expand/collapse by default.
9027 if (event.button !== 2)
9028 return;
9029
9030 var dataLoc = (gLoc === "post" ? "post" : "thumb");
9031 var data = collapse_sidebar_data[dataLoc];
9032 var collapse = data[name];
9033
9034 loadSettings();
9035
9036 if (collapse) {
9037 delete bbb.user.collapse_sidebar_data[dataLoc][name];
9038 delete data[name];
9039 }
9040 else
9041 bbb.user.collapse_sidebar_data[dataLoc][name] = data[name] = true;
9042
9043 saveSettings();
9044
9045 bbbNotice("The \"" + name + "\" section will now " + (!collapse ? "collapse" : "expand") + " by default.", 3);
9046
9047 event.preventDefault();
9048 }
9049
9050 function allowUserLimit() {
9051 // Allow use of the user thumbnail limit on the first page if there isn't a search limit and the current limit doesn't equal the user limit.
9052 var page = Number(getVar("page")) || 1; // When set to 0 or undefined, the first page is shown.
9053 var queryLimit = getQueryLimit();
9054 var searchLimit = getSearchLimit();
9055 var limit = (queryLimit !== undefined ? queryLimit : searchLimit) || thumbnail_count_default;
9056 var allowedLoc = (gLoc === "search" || gLoc === "favorites");
9057
9058 if (allowedLoc && thumbnail_count && thumbnail_count !== limit && page === 1 && (searchLimit === undefined || queryLimit !== undefined))
9059 return true;
9060 else
9061 return false;
9062 }
9063
9064 function noResultsPage(docEl) {
9065 // Check whether a page has zero results on it.
9066 var target = docEl || document.body;
9067 var numPosts = getPosts(target).length;
9068 var thumbContainer = getThumbContainer(gLoc, target) || target;
9069 var thumbContainerText = (thumbContainer ? thumbContainer.textContent : "");
9070
9071 if (!numPosts && thumbContainerText.indexOf("Nobody here but us chickens") > -1)
9072 return true;
9073 else
9074 return false;
9075 }
9076
9077 function danbLoc(url) {
9078 // Test a URL to find which section of Danbooru the script is running on.
9079 var target; // If/else variable.
9080
9081 if (url)
9082 target = new URL(url);
9083 else
9084 target = location;
9085
9086 var path = target.pathname;
9087 var query = target.search;
9088
9089 if (/\/posts\/\d+/.test(path))
9090 return "post";
9091 else if (/^\/(?:posts|$)/.test(path))
9092 return "search";
9093 else if (/^\/notes\/?$/.test(path) && query.indexOf("group_by=note") < 0)
9094 return "notes";
9095 else if (/\/comments\/\d+/.test(path))
9096 return "comment";
9097 else if (/^\/comments\/?$/.test(path)) {
9098 if (query.indexOf("group_by=comment") < 0)
9099 return "comments";
9100 else // This may need to be more specific in the future.
9101 return "comment_search";
9102 }
9103 else if (/\/explore\/posts\/popular(?:\/?$|\?)/.test(path))
9104 return "popular";
9105 // else if (/\/explore\/posts\/popular_view(?:\/?$|\?)/.test(path))
9106 // return "popular_view";
9107 else if (/\/pools\/\d+(?:\/?$|\?)/.test(path))
9108 return "pool";
9109 else if (/\/favorite_groups\/\d+(?:\/?$|\?)/.test(path))
9110 return "favorite_group";
9111 else if (/\/pools\/gallery/.test(path))
9112 return "pool_gallery";
9113 else if (path.indexOf("/favorites") === 0)
9114 return "favorites";
9115 else if (path.indexOf("/uploads/new") === 0)
9116 return "upload";
9117 else if (path.indexOf("/pools/new") === 0)
9118 return "new_pool";
9119 else if (/\/forum_topics\/\d+/.test(path))
9120 return "topic";
9121 else if (path.indexOf("/explore/posts/intro") === 0)
9122 return "intro";
9123 else if (path.indexOf("/session/new") === 0)
9124 return "signin";
9125 else if (/\/users\/\d+\/edit/.test(path))
9126 return "settings";
9127 else
9128 return undefined;
9129 }
9130
9131 function isLoggedIn() {
9132 // Use Danbooru's meta tags to determine if a use is logged in.
9133 if (getMeta("current-user-id") !== "")
9134 return true;
9135 else
9136 return false;
9137 }
9138
9139 function noXML() {
9140 // Don't use XML requests on certain pages where it won't do any good.
9141 var limit = getLimit();
9142 var pageNum = getVar("page");
9143 var paginator = getPaginator();
9144 var thumbContainer = getThumbContainer(gLoc);
9145 var imgContainer = getPostContent().container;
9146
9147 if (!paginator && !thumbContainer && !imgContainer)
9148 return true;
9149 else if (gLoc === "search" || gLoc === "favorites") {
9150 if (limit === 0 || pageNum === "b1" || noResultsPage() || safebSearchTest())
9151 return true;
9152 }
9153 else if (gLoc === "comments") {
9154 if (pageNum === "b1" || noResultsPage())
9155 return true;
9156 }
9157 else if (gLoc === "pool" || gLoc === "favorite_group" || gLoc === "popular" || gLoc === "popular_view") {
9158 if (noResultsPage())
9159 return true;
9160 }
9161
9162 return false;
9163 }
9164
9165 function useAPI() {
9166 // Determine whether the API will be needed for any options.
9167 if (((!isGoldLevel() && (show_loli || show_shota || show_toddlercon || show_banned)) || (show_deleted && !deleted_shown)) && (isLoggedIn() || !bypass_api))
9168 return true;
9169 else
9170 return false;
9171 }
9172
9173 function isRandomSearch() {
9174 // Check whether the search uses "order:random" in it.
9175 return ((getTagVar("order") || "").toLowerCase() === "random");
9176 }
9177
9178 function removeInheritedStorage(key) {
9179 // Remove an inherited sessionStorage key for a new tab/window.
9180 if (window.opener && history.length === 1) {
9181 var state = history.state || {};
9182 var stateProperty = key + "_reset";
9183
9184 if (!state[stateProperty]) {
9185 sessionStorage.removeItem(key);
9186 state[stateProperty] = true;
9187 history.replaceState(state, "");
9188 }
9189 }
9190 }
9191
9192 function isGoldLevel() {
9193 // Determine whether the user is at the gold account level or higher.
9194 return (getUserData("level") >= 30);
9195 }
9196
9197 function isModLevel() {
9198 // Determine whether the user is at the moderator account level or higher.
9199 return (getUserData("level") >= 40);
9200 }
9201
9202 function accountSettingCheck(scriptSetting) {
9203 // Determine whether the script setting or account/anonymous setting should be used.
9204 var loggedIn = isLoggedIn();
9205 var setting; // If/else variable.
9206
9207 if (scriptSetting === "script_blacklisted_tags") {
9208 if ((loggedIn && override_blacklist === "always") || (!loggedIn && override_blacklist !== "disabled"))
9209 setting = bbb.user.script_blacklisted_tags;
9210 else
9211 setting = getMeta("blacklisted-tags") || "";
9212 }
9213 else if (scriptSetting === "post_resize") {
9214 if (loggedIn && !override_resize)
9215 setting = (getMeta("always-resize-images") === "true");
9216 else
9217 setting = bbb.user.post_resize;
9218 }
9219 else if (scriptSetting === "load_sample_first") {
9220 if (loggedIn && !override_sample)
9221 setting = (getMeta("default-image-size") === "large");
9222 else
9223 setting = bbb.user.load_sample_first;
9224 }
9225
9226 return setting;
9227 }
9228
9229 function accountUpdateWatch() {
9230 // Signal a Danbooru account setting check upon signing in or submitting settings. Check the settings upon the next page loading.
9231 if (gLoc === "signin" || gLoc === "settings") {
9232 var submit = document.querySelector("input[type=submit][name=commit]");
9233
9234 submit.addEventListener("click", function() {
9235 createCookie("bbb_acc_check", 1);
9236 }, false);
9237 }
9238 else if (getCookie().bbb_acc_check) {
9239 var userId = getMeta("current-user-id");
9240
9241 if (userId !== "")
9242 searchPages("account_check", userId);
9243 else
9244 createCookie("bbb_acc_check", 0, -1);
9245 }
9246 }
9247
9248 function safebPostTest(postObject) {
9249 // Test Safebooru's posts to see if they're censored or not.
9250 if (location.host.indexOf("safebooru") < 0)
9251 return false;
9252
9253 var postInfo; // If/else variable.
9254
9255 if (postObject instanceof HTMLElement) {
9256 // If dealing with an element, turn it into a simple post info object.
9257 if (postObject.tagName === "ARTICLE" || postObject.id === "image-container")
9258 postInfo = {rating: postObject.bbbInfo("rating"), tag_string: postObject.bbbInfo("tags")};
9259 }
9260 else
9261 postInfo = postObject;
9262
9263 // Posts with an explicit/questionable rating or censored tags are bad.
9264 if (postInfo.rating !== "s" || safebCensorTagTest(postInfo.tag_string))
9265 return true;
9266 else
9267 return false;
9268 }
9269
9270 function safebSearchTest() {
9271 // Test Safebooru's current search tags to see if there will be no results.
9272 if (location.host.indexOf("safebooru") < 0)
9273 return false;
9274
9275 var tags = getVar("tags");
9276 var i, il, tag; // Loop variables.
9277
9278 if (!tags)
9279 return false;
9280
9281 tags = decodeURIComponent(tags.replace(/(\+|%20)/g, " ")).split(" ");
9282
9283 // Split up the "any" (~) tags and "all" tags for testing.
9284 var allTags = "";
9285 var anyTags = [];
9286
9287 for (i = 0, il = tags.length; i < il; i++) {
9288 tag = tags[i];
9289
9290 if (tag.charAt(0) === "~")
9291 anyTags.push(tag.slice(1));
9292 else
9293 allTags += " " + tag;
9294 }
9295
9296 // If any one "all" tag is censored, all posts will be bad.
9297 if (safebCensorTagTest(allTags))
9298 return true;
9299
9300 // If any one "any" tag isn't censored, not all posts will be bad.
9301 var anyMatch = !!anyTags[0];
9302
9303 for (i = 0, il = anyTags.length; i < il; i++) {
9304 tag = anyTags[i];
9305
9306 if (!safebCensorTagTest(tag)) {
9307 anyMatch = false;
9308 break;
9309 }
9310 }
9311
9312 return anyMatch;
9313 }
9314
9315 function safebCensorTagTest(string) {
9316 // Test a tag string or search string on Safebooru to see if it contains any bad tags.
9317 if (typeof(string) !== "string")
9318 return false;
9319 else
9320 return /(?:^|\s)(?:toddlercon|toddler|diaper|tentacle|rape|bestiality|beastiality|lolita|loli|nude|shota|pussy|penis|-rating:s\S+|rating:(?:e|q)\S+)(?:$|\s)/i.test(string);
9321 }
9322
9323 function searchAdd() {
9324 // Choose the appropriate search link option to run.
9325 if (search_add === "disabled" || (gLoc !== "search" && gLoc !== "post" && gLoc !== "favorites"))
9326 return;
9327
9328 searchAddRemove();
9329
9330 if (search_add === "link")
9331 searchAddLink();
9332 else if (search_add === "toggle")
9333 searchAddToggleLink();
9334 }
9335
9336 function searchAddRemove() {
9337 // Completely remove existing + and - tag links along with the whitespace after them.
9338 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
9339
9340 if (!tagList)
9341 return;
9342
9343 var addLinks = tagList.getElementsByClassName("search-inc-tag");
9344 var subLinks = tagList.getElementsByClassName("search-exl-tag");
9345 var blankRegEx = /^\s*$/;
9346
9347 while (addLinks[0]) {
9348 var addLink = addLinks[0];
9349 var addSibling = addLink.nextSibling;
9350 var addParent = addSibling.parentNode;
9351
9352 if (addSibling && addSibling.nodeType === 3 && blankRegEx.test(addSibling.nodeValue))
9353 addParent.removeChild(addSibling);
9354
9355 addParent.removeChild(addLink);
9356 }
9357
9358 while (subLinks[0]) {
9359 var subLink = subLinks[0];
9360 var subSibling = subLink.nextSibling;
9361 var subParent = subSibling.parentNode;
9362
9363 if (subSibling && subSibling.nodeType === 3 && blankRegEx.test(subSibling.nodeValue))
9364 subParent.removeChild(subSibling);
9365
9366 subParent.removeChild(subLink);
9367 }
9368 }
9369
9370 function searchAddLink() {
9371 // Add + and - links to the sidebar tag list for modifying searches.
9372 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
9373
9374 if (!tagList)
9375 return;
9376
9377 var tagItems = tagList.getElementsByTagName("li");
9378 var curTag = getCurTags();
9379 var curTagString = (curTag ? "+" + curTag : "");
9380
9381 for (var i = 0, il = tagItems.length; i < il; i++) {
9382 var tagItem = tagItems[i];
9383 var tagLink = tagItem.getElementsByClassName("search-tag")[0];
9384 var tagString = getVar("tags", tagLink.href);
9385 var tagFrag = document.createDocumentFragment();
9386
9387 var addTag = document.createElement("a");
9388 addTag.href = "/posts?tags=" + tagString + curTagString;
9389 addTag.innerHTML = "+";
9390 addTag.className = "search-inc-tag";
9391 tagFrag.appendChild(addTag);
9392
9393 var addSpace = document.createTextNode(" ");
9394 tagFrag.appendChild(addSpace);
9395
9396 var subTag = document.createElement("a");
9397 subTag.href = "/posts?tags=-" + tagString + curTagString;
9398 subTag.innerHTML = "–";
9399 subTag.className = "search-exl-tag";
9400 tagFrag.appendChild(subTag);
9401
9402 var subSpace = document.createTextNode(" ");
9403 tagFrag.appendChild(subSpace);
9404
9405 tagItem.insertBefore(tagFrag, tagLink);
9406 }
9407 }
9408
9409 function searchAddToggleLink() {
9410 // Add toggle links to the sidebar tag list for modifying the search box value.
9411 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
9412
9413 if (!tagList)
9414 return;
9415
9416 var tagItems = tagList.getElementsByTagName("li");
9417 var firstItem = tagItems[0];
9418 var toggleWidth; // If/else variable.
9419
9420 // Find a set width for the toggle link.
9421 if (firstItem) {
9422 var testItem = document.createElement("li");
9423 testItem.className = "category-0";
9424 testItem.style.height = "0px";
9425 testItem.style.visibility = "hidden";
9426
9427 var testLink = document.createElement("a");
9428 testLink.href = "#";
9429 testLink.style.display = "inline-block";
9430 testItem.appendChild(testLink);
9431
9432 firstItem.parentNode.appendChild(testItem);
9433
9434 testLink.innerHTML = "-";
9435 var subWidth = testLink.clientWidth;
9436
9437 testLink.innerHTML = "+";
9438 var addWidth = testLink.clientWidth;
9439
9440 testLink.innerHTML = "~";
9441 var orWidth = testLink.clientWidth;
9442
9443 toggleWidth = Math.max(subWidth, addWidth, orWidth);
9444
9445 firstItem.parentNode.removeChild(testItem);
9446 }
9447
9448 // Create and insert the toggle links.
9449 for (var i = 0, il = tagItems.length; i < il; i++) {
9450 var tagItem = tagItems[i];
9451 var tagLink = tagItem.getElementsByClassName("search-tag")[0];
9452 var tagString = decodeURIComponent(getVar("tags", tagLink.href));
9453 var tagFrag = document.createDocumentFragment();
9454 var tagFunc = searchAddToggle.bind(null, tagString);
9455
9456 var toggleTag = document.createElement("a");
9457 toggleTag.href = "/posts?tags=" + tagString;
9458 toggleTag.innerHTML = "»";
9459 toggleTag.style.display = "inline-block";
9460 toggleTag.style.textAlign = "center";
9461 toggleTag.style.width = toggleWidth + "px";
9462 toggleTag.addEventListener("click", tagFunc, false);
9463 toggleTag.addEventListener("mouseup", tagFunc, false);
9464 toggleTag.addEventListener("contextmenu", disableEvent, false);
9465 tagFrag.appendChild(toggleTag);
9466
9467 var toggleSpace = document.createTextNode(" ");
9468 tagFrag.appendChild(toggleSpace);
9469
9470 tagItem.insertBefore(tagFrag, tagLink);
9471 bbb.search_add.links[tagString] = toggleTag;
9472 }
9473
9474 // Watch various actions on the search box.
9475 var tagsInput = document.getElementById("tags");
9476
9477 if (tagsInput && (gLoc === "search" || gLoc === "post" || gLoc === "intro" || gLoc === "favorites")) {
9478 searchAddToggleCheck();
9479 tagsInput.addEventListener("input", searchAddToggleCheck, false);
9480 tagsInput.addEventListener("keyup", searchAddToggleCheck, false);
9481 tagsInput.addEventListener("cut", searchAddToggleCheck, false);
9482 tagsInput.addEventListener("paste", searchAddToggleCheck, false);
9483 tagsInput.addEventListener("change", searchAddToggleCheck, false);
9484 $(tagsInput).on("autocompleteselect", function(event) { delayMe(function(event) { searchAddToggleCheck(event); }); }); // Delayed to allow autocomplete to change the input.
9485 }
9486 }
9487
9488 function searchAddToggleCheck(event) {
9489 // Watch the search box value, test it upon changes, and update the tag links accordingly.
9490 var input = (event ? event.target : document.getElementById("tags"));
9491 var value = input.value;
9492 var oldValue = bbb.search_add.old;
9493 var i, il; // Loop variables.
9494
9495 if (oldValue !== value) {
9496 var tags = value.toLowerCase().bbbSpaceClean().split(/\s+/);
9497 var activeLinks = bbb.search_add.active_links;
9498
9499 for (i in activeLinks) {
9500 if (activeLinks.hasOwnProperty(i)) {
9501 var activeRegEx = new RegExp("(?:^|\\s)[-~]*" + escapeRegEx(i) + "(?:$|\\s)", "gi");
9502
9503 if (!activeRegEx.test(value))
9504 activeLinks[i].innerHTML = "»";
9505 }
9506 }
9507
9508 for (i = 0, il = tags.length; i < il; i++) {
9509 var tag = tags[i];
9510 var tagChar = tag.charAt(0);
9511 var tagType = "+";
9512
9513 if (tagChar === "-" || tagChar === "~") {
9514 tagType = (tagChar === "-" ? "–" : tagChar);
9515 tag = tag.slice(1);
9516 }
9517
9518 var tagLink = bbb.search_add.links[tag];
9519
9520 if (tagLink) {
9521 bbb.search_add.links[tag].innerHTML = tagType;
9522 bbb.search_add.active_links[tag] = tagLink;
9523 }
9524 }
9525
9526 bbb.search_add.old = value;
9527 }
9528 }
9529
9530 function searchAddToggle(tag, event) {
9531 // Modify the search box value based upon the tag link clicked and the tag's current state.
9532 var link = event.target;
9533 var button = event.button;
9534 var type = event.type;
9535 var linkType = link.innerHTML;
9536 var input = document.getElementById("tags");
9537 var inputValue = input.value;
9538 var tagRegEx = new RegExp("(^|\\s)[-~]*" + escapeRegEx(tag) + "(?=$|\\s)", "gi");
9539 var angleQuotes = String.fromCharCode(187);
9540 var enDash = String.fromCharCode(8211);
9541
9542 if ((type === "mouseup" && button !== 2) || (type === "click" && button !== 0)) // Don't respond to middle click and filter out duplicate user actions.
9543 return;
9544
9545 if (button === 2) // Immediately remove the tag upon a right click.
9546 linkType = "~";
9547
9548 // Each case changes the tag's toggle link display and updates the search box.
9549 switch (linkType) {
9550 case angleQuotes: // Tag currently not present.
9551 link.innerHTML = "+";
9552
9553 if (tagRegEx.test(inputValue))
9554 input.value = inputValue.replace(tagRegEx, tag).bbbSpaceClean();
9555 else
9556 input.value = (inputValue + " " + tag).bbbSpaceClean();
9557 break;
9558 case "+": // Tag currently included.
9559 link.innerHTML = "–";
9560
9561 if (tagRegEx.test(inputValue))
9562 input.value = inputValue.replace(tagRegEx, "$1-" + tag).bbbSpaceClean();
9563 else
9564 input.value = (inputValue + " -" + tag).bbbSpaceClean();
9565 break;
9566 case enDash: // Tag currently excluded.
9567 link.innerHTML = "~";
9568
9569 if (tagRegEx.test(inputValue))
9570 input.value = inputValue.replace(tagRegEx, "$1~" + tag).bbbSpaceClean();
9571 else
9572 input.value = (inputValue + " ~" + tag).bbbSpaceClean();
9573 break;
9574 case "~": // Tag currently included with other tags.
9575 link.innerHTML = angleQuotes;
9576
9577 if (tagRegEx.test(inputValue))
9578 input.value = inputValue.replace(tagRegEx, "$1").bbbSpaceClean();
9579 break;
9580 }
9581
9582 // Trigger the tag input width adjustment.
9583 try {
9584 $("#tags").keypress();
9585 }
9586 catch (error) {
9587 // Do nothing.
9588 }
9589
9590 event.preventDefault();
9591 }
9592
9593 function addPopularLink() {
9594 // Add the popular link back to the posts submenu.
9595 var subListItem = document.getElementById("subnav-hot") || document.getElementById("subnav-favorites");
9596
9597 if (!subListItem || !add_popular_link)
9598 return;
9599
9600 var popularListItem = document.createElement("li");
9601 popularListItem.id = "secondary-links-posts-popular";
9602
9603 var popularLink = document.createElement("a");
9604 popularLink.href = "/explore/posts/popular";
9605 popularLink.innerHTML = "Popular";
9606 popularListItem.appendChild(popularLink);
9607
9608 subListItem.parentNode.insertBefore(popularListItem, subListItem);
9609 }
9610
9611 function eraseSettingDialog() {
9612 // Open a dialog box for erasing various BBB information.
9613 var options = [
9614 {text: "Settings", func: function() { deleteData("bbb_settings"); removeMenu(); }},
9615 {text: "All Information", func: function() { eraseSettings(); removeMenu(); location.reload(); }}
9616 ];
9617
9618 var content = document.createDocumentFragment();
9619
9620 var header = document.createElement("h2");
9621 header.innerHTML = "Erase BBB Information";
9622 header.className = "bbb-header";
9623 content.appendChild(header);
9624
9625 var introText = document.createElement("div");
9626 introText.innerHTML = "Please select which information you would like to erase from below:";
9627 content.appendChild(introText);
9628
9629 var optionDiv = document.createElement("div");
9630 optionDiv.style.lineHeight = "1.5em";
9631 content.appendChild(optionDiv);
9632
9633 var cbFunc = function(checkbox, label, event) {
9634 if (event.button !== 0)
9635 return;
9636
9637 label.style.textDecoration = (checkbox.checked ? "none" : "line-through");
9638 };
9639
9640 for (var i = 0, il = options.length; i < il; i++) {
9641 var option = options[i];
9642
9643 var listLabel = document.createElement("label");
9644 listLabel.style.textDecoration = "line-through";
9645 optionDiv.appendChild(listLabel);
9646
9647 var listCheckbox = document.createElement("input");
9648 listCheckbox.type = "checkbox";
9649 listCheckbox.style.marginRight = "5px";
9650 listLabel.appendChild(listCheckbox);
9651
9652 var listText = document.createTextNode(option.text);
9653 listLabel.appendChild(listText);
9654
9655 var br = document.createElement("br");
9656 optionDiv.appendChild(br);
9657
9658 listLabel.addEventListener("click", cbFunc.bind(null, listCheckbox, listLabel), false);
9659 }
9660
9661 var okFunc = function() {
9662 var inputs = optionDiv.getElementsByTagName("input");
9663
9664 for (var i = 0, il = inputs.length; i < il; i++) {
9665 if (inputs[i].checked)
9666 options[i].func();
9667 }
9668 };
9669
9670 bbbDialog(content, {ok: okFunc, cancel: true});
9671 }
9672
9673 function localStorageDialog() {
9674 // Open a dialog box for cleaning out local storage for donmai.us.
9675 if (getCookie().bbb_ignore_storage)
9676 return;
9677
9678 var domains = [
9679 {url: "http://danbooru.donmai.us/", untrusted: false},
9680 {url: "https://danbooru.donmai.us/", untrusted: false},
9681 {url: "http://donmai.us/", untrusted: false},
9682 {url: "https://donmai.us/", untrusted: true},
9683 {url: "http://sonohara.donmai.us/", untrusted: false},
9684 {url: "https://sonohara.donmai.us/", untrusted: true},
9685 {url: "http://hijiribe.donmai.us/", untrusted: false},
9686 {url: "https://hijiribe.donmai.us/", untrusted: true},
9687 {url: "http://safebooru.donmai.us/", untrusted: false},
9688 {url: "https://safebooru.donmai.us/", untrusted: false},
9689 {url: "http://testbooru.donmai.us/", untrusted: false}
9690 ];
9691
9692 var content = document.createDocumentFragment();
9693
9694 var header = document.createElement("h2");
9695 header.innerHTML = "Local Storage Error";
9696 header.className = "bbb-header";
9697 content.appendChild(header);
9698
9699 var introText = document.createElement("div");
9700 introText.innerHTML = "While trying to save some settings, BBB has detected that your browser's local storage is full for the donmai.us domain and was unable to automatically fix the problem. In order for BBB to function properly, the storage needs to be cleaned out.<br><br> BBB can cycle through the various donmai locations and clear out Danbooru's autocomplete cache for each. Please select the domains/subdomains you'd like to clean from below and click OK to continue. If you click cancel, BBB will ignore the storage problems for the rest of this browsing session, but features may not work as expected.<br><br> <b>Notes:</b><ul><li>Three options in the domain list are not selected by default (marked as untrusted) since they require special permission from the user to accept invalid security certificates. However, if BBB detects you're already on one of these untrusted domains, then it will be automatically selected.</li><li>If you encounter this warning again right after storage has been cleaned, you may have to check domains you didn't check before or use the \"delete everything\" option to clear items in local storage besides autocomplete " + (window.bbbGMFunc ? "" : "and thumbnail ") + "info.</li></ul><br> <b>Donmai.us domains/subdomains:</b><br>";
9701 content.appendChild(introText);
9702
9703 var domainDiv = document.createElement("div");
9704 domainDiv.style.lineHeight = "1.5em";
9705 content.appendChild(domainDiv);
9706
9707 var cbFunc = function(event) {
9708 if (event.button !== 0)
9709 return;
9710
9711 var target = event.target;
9712
9713 target.nextSibling.style.textDecoration = (target.checked ? "none" : "line-through");
9714 };
9715
9716 for (var i = 0, il = domains.length; i < il; i++) {
9717 var domain = domains[i];
9718 var isChecked = (!domain.untrusted || location.href.indexOf(domain.url) > -1);
9719
9720 var listCheckbox = document.createElement("input");
9721 listCheckbox.name = domain.url;
9722 listCheckbox.type = "checkbox";
9723 listCheckbox.checked = isChecked;
9724 listCheckbox.style.marginRight = "5px";
9725 listCheckbox.addEventListener("click", cbFunc, false);
9726 domainDiv.appendChild(listCheckbox);
9727
9728 var listLink = document.createElement("a");
9729 listLink.innerHTML = domain.url + (domain.untrusted ? " (untrusted)" : "");
9730 listLink.href = domain.url;
9731 listLink.target = "_blank";
9732 listLink.style.textDecoration = (isChecked ? "none" : "line-through");
9733 domainDiv.appendChild(listLink);
9734
9735 var br = document.createElement("br");
9736 domainDiv.appendChild(br);
9737 }
9738
9739 var optionsText = document.createElement("div");
9740 optionsText.innerHTML = "<b>Options:</b><br>";
9741 optionsText.style.marginTop = "1em";
9742 content.appendChild(optionsText);
9743
9744 var optionsDiv = document.createElement("div");
9745 optionsDiv.style.lineHeight = "1.5em";
9746 content.appendChild(optionsDiv);
9747
9748 var compCheckbox = document.createElement("input");
9749 compCheckbox.name = "complete-delete";
9750 compCheckbox.type = "checkbox";
9751 compCheckbox.style.marginRight = "5px";
9752 optionsDiv.appendChild(compCheckbox);
9753
9754 var compText = document.createTextNode("Delete everything in local storage for each selection except for my BBB settings.");
9755 optionsDiv.appendChild(compText);
9756
9757 var okFunc = function() {
9758 var options = domainDiv.getElementsByTagName("input");
9759 var mode = (compCheckbox.checked ? "complete" : "normal");
9760 var selectedURLs = [];
9761 var origURL = location.href;
9762 var cleanCur = false;
9763 var session = new Date().getTime();
9764 var nextURL; // Loop variable.
9765
9766 for (var i = 0, il = options.length; i < il; i++) {
9767 var option = options[i];
9768
9769 if (option.checked) {
9770 if (origURL.indexOf(option.name) === 0)
9771 cleanCur = true;
9772 else if (!nextURL)
9773 nextURL = option.name;
9774 else
9775 selectedURLs.push(encodeURIComponent(option.name));
9776 }
9777 }
9778
9779 // Clean the current domain if it was selected.
9780 if (cleanCur)
9781 cleanLocalStorage(mode);
9782
9783 if (!nextURL) {
9784 // Retry saving if only the current domain was selected and do nothing if no domains were selected.
9785 if (cleanCur)
9786 retryLocalStorage();
9787 }
9788 else {
9789 // Start cycling through domains.
9790 bbbDialog("Currently cleaning local storage and loading the next domain. Please wait...", {ok: false, important: true});
9791 sessionStorage.bbbSetItem("bbb_local_storage_queue", JSON.stringify(bbb.local_storage_queue));
9792 location.href = updateURLQuery(nextURL + "posts/1/", {clean_storage: mode, clean_urls: selectedURLs.join(","), clean_origurl: encodeURIComponent(origURL), clean_session: session});
9793 }
9794 };
9795
9796 var cancelFunc = function() {
9797 createCookie("bbb_ignore_storage", 1);
9798 };
9799
9800 bbbDialog(content, {ok: okFunc, cancel: cancelFunc});
9801 }
9802
9803 function cleanLocalStorage(mode) {
9804 // Clean out various values in local storage.
9805 var i, keyName; // Loop variables.
9806
9807 if (mode === "autocomplete") {
9808 for (i = localStorage.length - 1; i >= 0; i--) {
9809 keyName = localStorage.key(i);
9810
9811 if (keyName.indexOf("ac-") === 0)
9812 localStorage.removeItem(keyName);
9813 }
9814 }
9815 else if (mode === "normal") {
9816 for (i = localStorage.length - 1; i >= 0; i--) {
9817 keyName = localStorage.key(i);
9818
9819 if (keyName.indexOf("ac-") === 0 || keyName === "bbb_thumb_cache")
9820 localStorage.removeItem(keyName);
9821 }
9822 }
9823 else if (mode === "complete") {
9824 for (i = localStorage.length - 1; i >= 0; i--) {
9825 keyName = localStorage.key(i);
9826
9827 if (keyName !== "bbb_settings")
9828 localStorage.removeItem(keyName);
9829 }
9830 }
9831 }
9832
9833 function retryLocalStorage() {
9834 // Try to save items to local storage that failed to get saved before.
9835 var sessLocal; // If/else variable.
9836
9837 if (sessionStorage.getItem("bbb_local_storage_queue")) {
9838 // Retrieve the local storage values from session storage after cycling through other domains.
9839 sessLocal = parseJson(sessionStorage.getItem("bbb_local_storage_queue"), {});
9840 sessionStorage.removeItem("bbb_local_storage_queue");
9841 }
9842 else if (bbb.local_storage_queue) {
9843 // If only the BBB storage object exists, assume the user selected to only clean the current domain and reset things.
9844 sessLocal = bbb.local_storage_queue;
9845 delete bbb.local_storage_queue;
9846 delete bbb.flags.local_storage_full;
9847 }
9848 else
9849 return;
9850
9851 for (var i in sessLocal) {
9852 if (sessLocal.hasOwnProperty(i))
9853 localStorage.bbbSetItem(i, sessLocal[i]);
9854 }
9855
9856 bbbNotice("Local storage cleaning has completed.", 6);
9857 }
9858
9859 function localStorageCheck() {
9860 // Check if the script is currently trying to manage local storage.
9861 var cleanMode = getVar("clean_storage");
9862 var cleanSession = Number(getVar("clean_session")) || 0;
9863 var session = new Date().getTime();
9864
9865 // Stop if the script is not currently cleaning storage or if an old URL is detected.
9866 if (!cleanMode || Math.abs(session - cleanSession) > 60000)
9867 return;
9868
9869 if (cleanMode !== "save") {
9870 // Cycle through the domains.
9871 var urls = getVar("clean_urls").split(",");
9872 var nextURL = urls.shift();
9873 var origURL = getVar("clean_origurl");
9874
9875 bbb.flags.local_storage_full = true; // Keep the cycled domains from triggering storage problems
9876 bbbDialog("Currently cleaning local storage and loading the next domain. Please wait...", {ok: false, important: true});
9877 history.replaceState(history.state, "", updateURLQuery(location.href, {clean_storage: undefined, clean_urls: undefined, clean_origurl: undefined, clean_session: undefined}));
9878 cleanLocalStorage(cleanMode);
9879
9880 if (nextURL)
9881 window.setTimeout(function() { location.href = updateURLQuery(decodeURIComponent(nextURL) + "posts/1/", {clean_storage: cleanMode, clean_urls: urls.join(), clean_origurl: origURL, clean_session: session}); }, 2000);
9882 else
9883 window.setTimeout(function() { location.href = updateURLQuery(decodeURIComponent(origURL), {clean_storage: "save", clean_session: session}); }, 2000);
9884 }
9885 else if (cleanMode === "save") {
9886 history.replaceState(history.state, "", updateURLQuery(location.href, {clean_storage: undefined, clean_session: undefined}));
9887 retryLocalStorage();
9888 }
9889 }
9890
9891 function loadData(key) {
9892 // Request info from GM_getValue and return it.
9893 var settings;
9894
9895 try {
9896 // If GM functions aren't available, throw an error.
9897 if (window.bbbGMFunc === false)
9898 throw false;
9899
9900 sendCustomEvent("bbbRequestLoad", {key: key, bbb: bbb});
9901 settings = bbb.gm_data;
9902 bbb.gm_data = undefined;
9903 }
9904 catch (error) {
9905 settings = localStorage.getItem(key);
9906 }
9907
9908 return settings || null;
9909 }
9910
9911 function saveData(key, value) {
9912 // Request saving info with GM_setValue.
9913 try {
9914 // If GM functions aren't available, throw an error.
9915 if (window.bbbGMFunc === false)
9916 throw false;
9917
9918 sendCustomEvent("bbbRequestSave", {key: key, value: value});
9919 }
9920 catch (error) {
9921 localStorage.bbbSetItem(key, value);
9922 }
9923 }
9924
9925 function deleteData(key) {
9926 // Request deleting info with GM_deleteValue.
9927 try {
9928 // If GM functions aren't available, throw an error.
9929 if (window.bbbGMFunc === false)
9930 throw false;
9931
9932 sendCustomEvent("bbbRequestDelete", {key: key});
9933 }
9934 catch (error) {
9935 localStorage.removeItem(key);
9936 }
9937 }
9938
9939 function listData() {
9940 // Request an info list array with GM_listValues.
9941 try {
9942 // If GM functions aren't available, throw an error.
9943 if (window.bbbGMFunc === false)
9944 throw false;
9945
9946 sendCustomEvent("bbbRequestList", {bbb: bbb});
9947
9948 var list = bbb.gm_data.split(",");
9949 bbb.gm_data = undefined;
9950
9951 return list;
9952 }
9953 catch (error) {
9954 return [];
9955 }
9956 }
9957
9958 function sendCustomEvent(type, detail) {
9959 // Try to send a custom event with the newest method and then the older method.
9960 try {
9961 window.dispatchEvent(new CustomEvent(type, {detail: detail, bubbles: false, cancelable: false}));
9962 }
9963 catch (error) {
9964 var customEvent = document.createEvent("CustomEvent");
9965 customEvent.initCustomEvent(type, false, false, detail);
9966 window.dispatchEvent(customEvent);
9967 }
9968 }
9969
9970 function getCookie() {
9971 // Return an associative array with cookie values.
9972 var data = document.cookie;
9973
9974 if(!data)
9975 return false;
9976
9977 data = data.split("; ");
9978 var out = {};
9979
9980 for (var i = 0, il = data.length; i < il; i++) {
9981 var temp = data[i].split("=");
9982 out[temp[0]] = temp[1];
9983 }
9984
9985 return out;
9986 }
9987
9988 function createCookie(cName, cValue, expDays) {
9989 // Generate a cookie with a expiration time in days.
9990 var data = cName + "=" + cValue + "; path=/";
9991
9992 if (expDays !== undefined) {
9993 var expDate = new Date();
9994 expDate.setTime(expDate.getTime() + expDays * 86400000);
9995 expDate.toUTCString();
9996 data += "; expires=" + expDate;
9997 }
9998
9999 document.cookie = data;
10000 }
10001
10002 function scrollbarWidth() {
10003 // Retrieve the scrollbar width by creating an element with scrollbars and finding the difference.
10004 var scroller = document.createElement("div");
10005 scroller.style.width = "150px";
10006 scroller.style.height = "150px";
10007 scroller.style.visibility = "hidden";
10008 scroller.style.overflow = "scroll";
10009 scroller.style.position = "absolute";
10010 scroller.style.top = "0px";
10011 scroller.style.left = "0px";
10012 document.body.appendChild(scroller);
10013 var scrollDiff = scroller.offsetWidth - scroller.clientWidth;
10014 document.body.removeChild(scroller);
10015
10016 return scrollDiff;
10017 }
10018
10019 function bbbIsNum(value) {
10020 // Strictly test for a specific style of number.
10021 return /^-?\d+(\.\d+)?$/.test(value);
10022 }
10023
10024 function isNumMetatag(tag) {
10025 // Check if the tag from a search string is a numeric metatag.
10026 if (tag.indexOf(":") < 0)
10027 return false;
10028 else {
10029 var tagName = tag.split(":", 1)[0];
10030
10031 if (tagName === "score" || tagName === "favcount" || tagName === "id" || tagName === "width" || tagName === "height")
10032 return true;
10033 else
10034 return false;
10035 }
10036 }
10037
10038 function isMetatag(tag) {
10039 // Check if the tag from a search string is a metatag.
10040 if (tag.indexOf(":") < 0)
10041 return false;
10042 else {
10043 var tagName = tag.split(":", 1)[0].bbbSpaceClean();
10044
10045 if (tagName === "pool" || tagName === "user" || tagName === "status" || tagName === "rating" || tagName === "parent" || tagName === "child" || tagName === "isfav" || tagName === "userid" || tagName === "taggerid" || tagName === "source" || tagName === "approverid" || tagName === "filetype")
10046 return true;
10047 else
10048 return false;
10049 }
10050 }
10051
10052 function delayMe(func) {
10053 // Run the function after the browser has finished its current stack of tasks.
10054 window.setTimeout(func, 0);
10055 }
10056
10057 function escapeRegEx(regEx) {
10058 // Replace special characters with escaped versions to make them safe for RegEx.
10059 return regEx.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
10060 }
10061
10062 function updateURLQuery(url, newQueries) {
10063 // Update the query portion of a URL. If a param isn't declared, it will be added. If it is, it will be updated.
10064 // Assigning undefined to a param that exists will remove it. Assigning null to a param that exists will completely remove its value. Assigning null to a new param will leave it with no assigned value.
10065 var urlParts = url.split("#", 1)[0].split("?", 2);
10066 var urlQuery = urlParts[1] || "";
10067 var queries = urlQuery.split("&");
10068 var queryObj = {};
10069 var i, il, query, queryName, queryValue; // Loop variables.
10070
10071 for (i = 0, il = queries.length; i < il; i++) {
10072 query = queries[i].split("=");
10073 queryName = query[0];
10074 queryValue = query[1];
10075
10076 if (queryName)
10077 queryObj[queryName] = queryValue;
10078 }
10079
10080 for (i in newQueries) {
10081 if (newQueries.hasOwnProperty(i))
10082 queryObj[i] = newQueries[i];
10083 }
10084
10085 queries.length = 0;
10086
10087 for (i in queryObj) {
10088 if (queryObj.hasOwnProperty(i)) {
10089 queryValue = queryObj[i];
10090
10091 if (queryValue === null) // Declared param with no assigned value.
10092 query = i;
10093 else if (queryValue === undefined) // Undeclared.
10094 query = undefined;
10095 else // Declared param with an assigned value (including empty srings).
10096 query = i + "=" + queryValue;
10097
10098 if (query !== undefined) // Omit undefined params.
10099 queries.push(query);
10100 }
10101 }
10102
10103 urlQuery = queries.join("&");
10104
10105 return (urlParts[0] + (urlQuery ? "?" + urlQuery : ""));
10106 }
10107
10108 function timestamp(format) {
10109 // Returns a simple timestamp based on the format string provided. String placeholders: y = year, m = month, d = day, hh = hours, mm = minutes, ss = seconds, ms = milliseconds
10110 function padDate(number) {
10111 // Adds a leading "0" to single digit values.
10112 var numString = String(number);
10113
10114 if (numString.length === 1)
10115 numString = "0" + numString;
10116
10117 return numString;
10118 }
10119
10120 var stamp = format || "y-m-d hh:mm:ss";
10121 var time = new Date();
10122 var year = time.getFullYear();
10123 var month = padDate(time.getMonth() + 1);
10124 var day = padDate(time.getDate());
10125 var hours = padDate(time.getHours());
10126 var minutes = padDate(time.getMinutes());
10127 var seconds = padDate(time.getSeconds());
10128 var milliseconds = padDate(time.getMilliseconds());
10129
10130 stamp = stamp.replace("hh", hours).replace("mm", minutes).replace("ss", seconds).replace("y", year).replace("m", month).replace("d", day).replace("ms", milliseconds);
10131
10132 return stamp;
10133 }
10134
10135 function isOldVersion(ver) {
10136 // Takes the provided version and compares it to the script version. Returns true if the provided version is older than the script version.
10137 var userVer = ver || bbb.user.bbb_version;
10138 var scriptVer = bbb.options.bbb_version;
10139 var userNums = userVer.split(".");
10140 var userLength = userNums.length;
10141 var scriptNums = scriptVer.split(".");
10142 var scriptLength = scriptNums.length;
10143 var loopLength = (userLength > scriptLength ? userLength : scriptLength);
10144
10145 for (var i = 0; i < loopLength; i++) {
10146 var userNum = (userNums[i] ? Number(userNums[i]) : 0);
10147 var scriptNum = (scriptNums[i] ? Number(scriptNums[i]) : 0);
10148
10149 if (userNum < scriptNum)
10150 return true;
10151 else if (scriptNum < userNum)
10152 return false;
10153 }
10154
10155 return false;
10156 }
10157
10158 function uniqueIdNum() {
10159 // Return a unique ID number for an element.
10160 if (!bbb.uId)
10161 bbb.uId = 1;
10162 else
10163 bbb.uId++;
10164
10165 return "bbbuid" + bbb.uId;
10166 }
10167
10168 function loadMetaGroups() {
10169 // Load a user's group metatags.
10170 if (bbb.groups === undefined) {
10171 bbb.groups = {};
10172
10173 for (var i = 0, il = tag_groups.length; i < il; i++) {
10174 var userGroup = tag_groups[i];
10175
10176 bbb.groups[userGroup.name] = userGroup.tags;
10177 }
10178 }
10179 }
10180
10181 function bbbAutocompleteInit() {
10182 // Set up a modified version of Danbooru's autocomplete.
10183 try {
10184 var Autocomplete = bbb.autocomplete;
10185
10186 Autocomplete.AUTOCOMPLETE_VERSION = 1;
10187 Autocomplete.PREFIXES = /^([-~]*)(.*)$/i;
10188 Autocomplete.METATAGS = /^(status|rating|parent|child|user|pool|group|g|isfav|userid|taggerid|approverid|source|id|score|favcount|height|width|filetype):(.*)$/i;
10189
10190 Autocomplete.initialize_all = function() {
10191 if (Danbooru.Utility.meta("enable-auto-complete") === "true") {
10192 $.widget("ui.autocomplete", $.ui.autocomplete, {
10193 options: {delay: 0, minLength: 1, autoFocus: false, focus: function() { return false; }},
10194 _create: function() {
10195 this.element.on("keydown.Autocomplete.tab", null, "tab", Autocomplete.on_tab);
10196 this._super();
10197 },
10198 _renderItem: Autocomplete.render_item,
10199 });
10200 }
10201 };
10202
10203 Autocomplete.normal_source = function(term, resp) {
10204 $.ajax({
10205 url: "/tags/autocomplete.json",
10206 data: {"search[name_matches]": term, "expiry": 7},
10207 method: "get",
10208 success: function(data) {
10209 var d = $.map(data, function(tag) {
10210 return {type: "tag", label: tag.name.replace(/_/g, " "), antecedent: tag.antecedent_name, value: tag.name, category: tag.category, post_count: tag.post_count};
10211 });
10212
10213 resp(d);
10214 }
10215 });
10216 };
10217
10218 Autocomplete.parse_query = function(text, caret) {
10219 var metatag = "";
10220 var term = "";
10221 var before_caret_text = text.substring(0, caret);
10222 var match = before_caret_text.match(/\S+$/g);
10223
10224 if (match)
10225 term = match[0];
10226 else
10227 return {};
10228
10229 if (!!(match = term.match(Autocomplete.PREFIXES))) {
10230 metatag = match[1].toLowerCase();
10231 term = match[2];
10232 }
10233
10234 if (!!(match = term.match(Autocomplete.METATAGS))) {
10235 metatag = match[1].toLowerCase();
10236 term = match[2];
10237 }
10238
10239 return {metatag: metatag, term: term};
10240 };
10241
10242 Autocomplete.insert_completion = function(input, completion) {
10243 var before_caret_text = input.value.substring(0, input.selectionStart);
10244 var after_caret_text = input.value.substring(input.selectionStart);
10245
10246 var prefixes = "-~";
10247 var regexp = new RegExp("([" + prefixes + "]*)\\S+$", "g");
10248 before_caret_text = before_caret_text.replace(regexp, "$1") + completion + " ";
10249
10250 input.value = before_caret_text + after_caret_text;
10251 input.selectionStart = input.selectionEnd = before_caret_text.length;
10252 };
10253
10254 Autocomplete.on_tab = function(event) {
10255 var input = this;
10256 var autocomplete = $(input).autocomplete("instance");
10257 var $autocomplete_menu = autocomplete.menu.element;
10258
10259 if (!$autocomplete_menu.is(":visible"))
10260 return;
10261
10262 if ($autocomplete_menu.has(".ui-state-active").length === 0) {
10263 var $first_item = $autocomplete_menu.find(".ui-menu-item").first();
10264 var completion = $first_item.data().uiAutocompleteItem.value;
10265
10266 Autocomplete.insert_completion(input, completion);
10267 autocomplete.close();
10268 }
10269
10270 event.preventDefault();
10271 };
10272
10273 Autocomplete.render_item = function(list, item) {
10274 var $link = $("<a/>");
10275 $link.text(item.label);
10276 $link.attr("href", "/posts?tags=" + encodeURIComponent(item.value));
10277 $link.click(function(e) {e.preventDefault();});
10278
10279 if (item.antecedent) {
10280 var antecedent = item.antecedent.replace(/_/g, " ");
10281 var arrow = $("<span/>").html(" → ").addClass("autocomplete-arrow");
10282 var antecedent_element = $("<span/>").text(antecedent).addClass("autocomplete-antecedent");
10283
10284 $link.prepend([antecedent_element, arrow]);
10285 }
10286
10287 if (item.post_count !== undefined) {
10288 var count = item.post_count;
10289
10290 if (count >= 1000)
10291 count = Math.floor(count / 1000) + "k";
10292
10293 var $post_count = $("<span/>").addClass("post-count").css("float", "right").text(count);
10294
10295 $link.append($post_count);
10296 }
10297
10298 if (item.type === "tag" || item.type === "metatag")
10299 $link.addClass("tag-type-" + item.category);
10300 else if (item.type === "user") {
10301 var level_class = "user-" + item.level.toLowerCase();
10302
10303 $link.addClass(level_class);
10304
10305 if (Danbooru.Utility.meta("style-usernames") === "true")
10306 $link.addClass("with-style");
10307 }
10308
10309 var $menu_item = $("<div/>").append($link);
10310
10311 return $("<li/>").data("item.autocomplete", item).append($menu_item).appendTo(list);
10312 };
10313
10314 var groups = [];
10315
10316 for (var i = 0, il = tag_groups.length; i < il; i++)
10317 groups.push(tag_groups[i].name);
10318
10319 Autocomplete.static_metatags = {
10320 status: ["any", "deleted", "active", "pending", "flagged", "banned"],
10321 rating: ["safe", "questionable", "explicit"],
10322 child: ["any", "none"],
10323 parent: ["any", "none"],
10324 filetype: ["jpg", "png", "gif", "swf", "zip", "webm", "mp4"],
10325 isfav: ["true", "false"],
10326 pool: ["series", "collection", "any", "none", "active", "inactive"],
10327 source: ["any", "none"],
10328 approverid: ["any", "none"],
10329 group: groups,
10330 g: groups
10331 };
10332
10333 Autocomplete.static_metatag_source = function(term, resp, metatag) {
10334 var sub_metatags = this.static_metatags[metatag];
10335
10336 var regexp = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
10337 var matches = $.grep(sub_metatags, function (sub_metatag) {
10338 return regexp.test(sub_metatag);
10339 });
10340
10341 var d = $.map(matches, function(sub_metatag) {
10342 return {type: "metatag", category: 5, label: sub_metatag, value: metatag + ":" + sub_metatag};
10343 });
10344
10345 if (d.length > 10)
10346 d = d.slice(0, 10);
10347
10348 resp(d);
10349 };
10350
10351 Autocomplete.user_source = function(term, resp, metatag) {
10352 $.ajax({
10353 url: "/users.json",
10354 data: {"search[order]": "post_upload_count", "search[current_user_first]": "true", "search[name_matches]": term + "*", "limit": 10},
10355 method: "get",
10356 success: function(data) {
10357 var display_name = function(name) {return name.replace(/_/g, " ");};
10358
10359 resp($.map(data, function(user) {
10360 return {type: "user", label: display_name(user.name), value: metatag + ":" + user.name, level: user.level_string};
10361 }));
10362 }
10363 });
10364 };
10365
10366 Autocomplete.initialize_all();
10367 }
10368 catch (error) {
10369 bbbNotice("Unexpected error while trying to initialize autocomplete. (Error: " + error.message + ")", -1);
10370 }
10371
10372 }
10373
10374 function bbbAutocomplete(searchInputs) {
10375 // Apply a modified copy of Danbooru's autocomplete to inputs after Danbooru has finished.
10376 delayMe(function() {
10377 if (!bbb.autocomplete.AUTOCOMPLETE_VERSION)
10378 bbbAutocompleteInit();
10379
10380 try {
10381 var Autocomplete = bbb.autocomplete;
10382 var $fields_multiple = $(searchInputs);
10383
10384 $fields_multiple.autocomplete({
10385 search: function() {
10386 if ($(this).data("ui-autocomplete"))
10387 $(this).data("ui-autocomplete").menu.bindings = $();
10388 },
10389 select: function(event, ui) {
10390 if (event.key === "Enter")
10391 event.stopImmediatePropagation();
10392
10393 Autocomplete.insert_completion(this, ui.item.value);
10394 return false;
10395 },
10396 source: function(req, resp) {
10397 var query = Autocomplete.parse_query(req.term, this.element.get(0).selectionStart);
10398 var metatag = query.metatag;
10399 var term = query.term;
10400
10401 if (!metatag && !term) {
10402 this.close();
10403 return;
10404 }
10405
10406 switch (metatag) {
10407 case "userid":
10408 case "taggerid":
10409 case "id":
10410 case "score":
10411 case "favcount":
10412 case "height":
10413 case "width":
10414 resp([]);
10415 return;
10416 case "status":
10417 case "rating":
10418 case "parent":
10419 case "child":
10420 case "group":
10421 case "g":
10422 case "isfav":
10423 case "pool":
10424 case "source":
10425 case "filetype":
10426 case "approverid":
10427 Autocomplete.static_metatag_source(term, resp, metatag);
10428 return;
10429 case "user":
10430 Autocomplete.user_source(term, resp, metatag);
10431 break;
10432 default:
10433 Autocomplete.normal_source(term, resp);
10434 break;
10435 }
10436 }
10437 });
10438
10439 // Make autocomplete fixed like the quick search and menu and allow it to be on top of inputs if there is more space there.
10440 $(searchInputs).autocomplete("widget").css("position", "fixed");
10441 $(searchInputs).autocomplete("option", "position", {my: "left top", at: "left bottom", collision: "flip"});
10442 }
10443 catch (error) {
10444 bbbNotice("Unexpected error while trying to initialize autocomplete. (Error: " + error.message + ")", -1);
10445 }
10446 });
10447 }
10448
10449 function menuAutocomplete(searchInputs) {
10450 // Use autocomplete on a BBB menu input if allowed by the option.
10451 if (enable_menu_autocomplete)
10452 bbbAutocomplete(searchInputs);
10453 }
10454
10455 function otherAutocomplete(searchInputs) {
10456 // Use autocomplete on an input outside of the BBB menu if allowed for Danbooru.
10457 var allowAutocomplete = (getMeta("enable-auto-complete") === "true");
10458
10459 if (allowAutocomplete)
10460 bbbAutocomplete(searchInputs);
10461 }
10462
10463} // End of bbbScript.
10464
10465function runBBBScript() {
10466 // Run the script or prep it to run when Danbooru's JS is ready.
10467 if (document.readyState === "interactive" && typeof(Danbooru) === "undefined") {
10468 var danbScript = document.querySelector("script[src^='/assets/application-']");
10469
10470 if (danbScript)
10471 danbScript.addEventListener("load", bbbScript, true);
10472 }
10473 else
10474 bbbScript();
10475}
10476
10477function bbbInit() {
10478 var bbbGMFunc; // If/else variable;
10479
10480 // Set up the data storage.
10481 if (typeof(GM_getValue) === "undefined" || typeof(GM_getValue("bbbdoesntexist", "default")) === "undefined")
10482 bbbGMFunc = false;
10483 else {
10484 bbbGMFunc = true;
10485
10486 if (typeof(GM_deleteValue) === "undefined") {
10487 GM_deleteValue = function(key) {
10488 GM_setValue(key, "");
10489 };
10490 }
10491
10492 if (typeof(GM_listValues) === "undefined") {
10493 GM_listValues = function() {
10494 return ["bbb_settings"];
10495 };
10496 }
10497
10498 window.addEventListener("bbbRequestLoad", function(event) {
10499 var key = event.detail.key;
10500 var bbbObj = event.detail.bbb;
10501
10502 if (typeof(key) === "string" && key === "bbb_settings" && typeof(bbbObj) === "object") {
10503 var value = GM_getValue(key);
10504
10505 if (typeof(value) === "string")
10506 bbbObj.gm_data = value;
10507 }
10508 }, false);
10509
10510 window.addEventListener("bbbRequestSave", function(event) {
10511 var key = event.detail.key;
10512 var value = event.detail.value;
10513
10514 if (typeof(key) === "string" && key === "bbb_settings" && typeof(value) === "string" && value.length <= 2500000)
10515 GM_setValue(key, value);
10516 }, false);
10517
10518 window.addEventListener("bbbRequestDelete", function(event) {
10519 var key = event.detail.key;
10520
10521 if (typeof(key) === "string")
10522 GM_deleteValue(key);
10523 }, false);
10524
10525 window.addEventListener("bbbRequestList", function(event) {
10526 var bbbObj = event.detail.bbb;
10527
10528 if (typeof(bbbObj) === "object")
10529 bbbObj.gm_data = GM_listValues().join(","); // Can't pass an array/object back to the page scope, so change it into a string for now.
10530 }, false);
10531 }
10532
10533 // Inject the script.
10534 var script = document.createElement('script');
10535 script.type = "text/javascript";
10536 script.appendChild(document.createTextNode('window.bbbGMFunc = ' + bbbGMFunc + '; ' + bbbScript + '(' + runBBBScript + ')();'));
10537 document.body.appendChild(script);
10538 window.setTimeout(function() { document.body.removeChild(script); }, 0);
10539}
10540
10541bbbInit();