· 10 years ago · Mar 30, 2016, 08:30 PM
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. Including the viewing of hidden/censored images on non-upgraded accounts and more.
6// @version 7.2.3
7// @match *://*.donmai.us/*
8// @run-at document-end
9// @grant none
10// @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
11// ==/UserScript==
12
13// Have a nice day. - A Pseudonymous Coder
14
15function bbbScript() { // This is needed to make this script work in Chrome.
16 /*
17 * NOTE: You no longer need to edit this script to change settings!
18 * Use the "BBB Settings" button in the menu instead.
19 */
20
21 // If Danbooru's JS isn't available, assume we're somewhere this script isn't needed and stop.
22 if (typeof(Danbooru) === "undefined")
23 return;
24
25 /* Helper Prototypes */
26 // Don't get hoisted so they should be declared at the top to simplify things.
27 String.prototype.bbbSpacePad = function() {
28 // Add a leading and trailing space.
29 return (this.length ? " " + this + " " : "");
30 };
31
32 String.prototype.bbbSpaceClean = function() {
33 // Remove leading, trailing, and multiple spaces.
34 return this.replace(/\s+/g, " ").replace(/^\s|\s$/g, "");
35 };
36
37 String.prototype.bbbTagClean = function() {
38 // Remove extra commas along with leading, trailing, and multiple spaces.
39 return this.replace(/[\s,]*(%\))\s*|\s*([~-]*\(%)[\s,]*/g, " $& ").replace(/[\s,]*,[\s,]*/g, ", ").replace(/[\s,]+$|^[\s,]+/g, "").replace(/\s+/g, " ");
40 };
41
42 String.prototype.bbbHash = function() {
43 // Turn a string into a hash using the current Danbooru hash method.
44 var hash = 5381;
45 var i = this.length;
46
47 while(i)
48 hash = (hash * 33) ^ this.charCodeAt(--i);
49
50 return hash >>> 0;
51 };
52
53 Element.prototype.bbbGetPadding = function() {
54 // Get all the padding measurements of an element including the total width and height.
55 if (window.getComputedStyle) {
56 var computed = window.getComputedStyle(this, null);
57 var paddingLeft = parseFloat(computed.paddingLeft);
58 var paddingRight = parseFloat(computed.paddingRight);
59 var paddingTop = parseFloat(computed.paddingTop);
60 var paddingBottom = parseFloat(computed.paddingBottom);
61 var paddingHeight = paddingTop + paddingBottom;
62 var paddingWidth = paddingLeft + paddingRight;
63 return {width: paddingWidth, height: paddingHeight, top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight};
64 }
65 };
66
67 Element.prototype.bbbHasClass = function() {
68 // Test an element for one or more collections of classes.
69 var classList = this.classList;
70
71 for (var i = 0, il = arguments.length; i < il; i++) {
72 var classes = arguments[i].bbbSpaceClean();
73
74 if (!classes)
75 continue;
76
77 var classArray = classes.split(" ");
78 var hasClass = true;
79
80 for (var j = 0, jl = classArray.length; j < jl; j++) {
81 if (!classList.contains(classArray[j])) {
82 hasClass = false;
83 break;
84 }
85 }
86
87 if (hasClass)
88 return true;
89 }
90
91 return false;
92 };
93
94 Element.prototype.bbbAddClass = function(classString) {
95 // Add one or more classes to an element.
96 var classes = classString.bbbSpaceClean();
97
98 if (!classes)
99 return;
100
101 var classList = this.classList;
102 var classArray = classes.split(" ");
103
104 for (var i = 0, il = classArray.length; i < il; i++)
105 classList.add(classArray[i]);
106 };
107
108 Element.prototype.bbbRemoveClass = function(classString) {
109 // Remove one or more classes from an element.
110 var classes = classString.bbbSpaceClean();
111
112 if (!classes)
113 return;
114
115 var classList = this.classList;
116 var classArray = classes.split(" ");
117
118 for (var i = 0, il = classArray.length; i < il; i++)
119 classList.remove(classArray[i]);
120 };
121
122 Element.prototype.bbbWatchNodes = function(func) {
123 // Watch for new nodes.
124 var observer = window.MutationObserver || window.WebKitMutationObserver;
125
126 if (observer) {
127 observer = new observer(func);
128 observer.observe(this, {childList: true, subtree: true});
129 }
130 else
131 this.addEventListener("DOMNodeInserted", func, false);
132 };
133
134 Storage.prototype.bbbSetItem = function(key, value) {
135 // Store a value in storage and warn if it is full.
136 try {
137 this.setItem(key, value);
138 }
139 catch (error) {
140 if (error.code === 22 || error.code === 1014) {
141 if (this === localStorage) {
142 if (!bbb.flags.local_storage_full) {
143 if (localStorage.length > 2000) {
144 // Try clearing out autocomplete if that appears to be the problem.
145 cleanLocalStorage("autocomplete");
146
147 try {
148 localStorage.setItem(key, value);
149 }
150 catch (localError) {
151 bbb.flags.local_storage_full = true;
152 }
153 }
154 else
155 bbb.flags.local_storage_full = true;
156
157 // Store the local storage value until it can be retried.
158 if (bbb.flags.local_storage_full) {
159 bbb.local_storage_queue = {};
160 bbb.local_storage_queue[key] = value;
161 localStorageDialog();
162 }
163 }
164 else {
165 // Temporarily store additional local storage values until they can be retried.
166 if (sessionStorage.getItem("bbb_local_storage_queue")) {
167 var sessLocal = JSON.parse(sessionStorage.getItem("bbb_local_storage_queue"));
168
169 sessLocal[key] = value;
170 sessionStorage.bbbSetItem("bbb_local_storage_queue", JSON.stringify(sessLocal));
171 }
172 else
173 bbb.local_storage_queue[key] = value;
174 }
175 }
176 else {
177 // Keep only a few values in session storage.
178 for (var i = sessionStorage.length - 1; i >= 0; i--) {
179 var keyName = sessionStorage.key(i);
180
181 if (keyName !== "bbb_endless_default" && keyName !== "bbb_quick_search")
182 sessionStorage.removeItem(keyName);
183 }
184
185 try {
186 sessionStorage.setItem(key, value);
187 }
188 catch (sessionError) {
189 bbbNotice("Your settings/data could not be saved/updated. The browser's session storage is full.", -1);
190 }
191 }
192 }
193 else
194 bbbNotice("Unexpected error while attempting to save/update settings. (Error: " + error.message + ")", -1);
195 }
196 };
197
198 /* Global Variables */
199 var bbb = { // Container for script info.
200 blacklist: {
201 entries: [],
202 match_list: {},
203 smart_view_target: undefined
204 },
205 cache: { // Thumbnail info cache.
206 current: {
207 history: [],
208 names: {}
209 },
210 save_enabled: false,
211 stored: {}
212 },
213 custom_tag: {
214 searches: [],
215 style_list: {}
216 },
217 dialog: {
218 queue: []
219 },
220 drag_scroll: {
221 lastX: undefined,
222 lastY: undefined,
223 moved: false,
224 target: undefined
225 },
226 el: { // Script elements.
227 menu: {} // Menu elements.
228 },
229 endless: {
230 append_page: false,
231 enabled: false,
232 fill_first_page: false,
233 last_paginator: undefined,
234 new_paginator: undefined,
235 no_thumb_count: 0,
236 pages: [],
237 paused: false,
238 posts: {}
239 },
240 fixed_paginator_space: 0,
241 fixed_sidebar: {
242 content: undefined,
243 left: undefined,
244 sidebar: undefined,
245 top: undefined
246 },
247 flags: {},
248 hotkeys: {
249 other: { // Hotkeys for misc locations.
250 66: {func: openMenu}, // B
251 69: {func: endlessToggle}, // E
252 70: {func: quickSearchOpen}, // F
253 s70: {func: quickSearchReset} // SHIFT + F
254 },
255 post: { // Post hotkeys.
256 49: {func: resizeHotkey, custom_handler: true}, // 1
257 50: {func: resizeHotkey, custom_handler: true}, // 2
258 51: {func: resizeHotkey, custom_handler: true}, // 3
259 52: {func: resizeHotkey, custom_handler: true}, // 4
260 66: {func: openMenu}, // B
261 78: {func: function(event) { // N
262 Danbooru.Note.TranslationMode.toggle(event);
263 translationModeToggle();
264 }},
265 86: {func: swapPost} // V
266 }
267 },
268 post: { // Post content info and status.
269 info: {}, // Post information object.
270 resize: {
271 mode: "none",
272 ratio: 1
273 },
274 swapped: false, // Whether the post content has been changed between the original and sample versions.
275 translation_mode: false
276 },
277 options: { // Setting options and data.
278 bbb_version: "7.2.3",
279 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."),
280 arrow_nav: newOption("checkbox", false, "Arrow Navigation", "Allow the use of the left and right arrow keys to navigate pages. <tiphead>Note</tiphead>This option has no effect on individual posts."),
281 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"]}),
282 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"]}),
283 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."),
284 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."),
285 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 the \"hidden\" or \"replaced\" post display options, 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."),
286 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"]}),
287 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."),
288 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."),
289 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"]}),
290 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"]}),
291 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"]}),
292 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."),
293 clean_links: newOption("checkbox", false, "Clean Links", "Remove the extra information after the post ID in thumbnail links.<tiphead>Note</tiphead>Enabling this option will disable Danbooru's search navigation and active pool/favorite group detection for posts."),
294 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."),
295 comment_score: newOption("checkbox", false, "Comment Scores", "Make comment scores visible by adding them as direct links to their respective comments."),
296 custom_status_borders: newOption("checkbox", false, "Custom Status Borders", "Override Danbooru's thumbnail borders for deleted, flagged, pending, parent, and child images."),
297 custom_tag_borders: newOption("checkbox", true, "Custom Tag Borders", "Add thumbnail borders to posts with specific tags."),
298 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."),
299 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."),
300 enable_status_message: newOption("checkbox", true, "Enable Status Message", "When requesting information from Danbooru, display the request status in the lower right corner."),
301 endless_default: newOption("dropdown", "disabled", "Default", "Enable endless pages on the favorites, search, pool, notes, 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.", {txtOptions:["Disabled:disabled", "Off:off", "On:on", "Paused:paused"]}),
302 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."),
303 endless_pause_interval: newOption("dropdown", 0, "Pause Interval", "Pause endless pages each time the number of pages reaches a multiple of the selected amount.", {txtOptions:["Disabled:0"], numRange:[1,100]}),
304 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."),
305 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."),
306 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]}),
307 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"]}),
308 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."),
309 fixed_paginator: newOption("dropdown", "disabled", "Fixed Paginator", "Make the paginator always visible for the favorites, search, pool, notes, 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"]}),
310 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"]}),
311 hide_ban_notice: newOption("checkbox", false, "Hide Ban Notice", "Hide the Danbooru ban notice."),
312 hide_comment_notice: newOption("checkbox", false, "Hide Comment Guide Notice", "Hide the Danbooru comment guide notice."),
313 hide_pool_notice: newOption("checkbox", false, "Hide Pool Guide Notice", "Hide the Danbooru pool guide notice."),
314 hide_sign_up_notice: newOption("checkbox", false, "Hide Sign Up Notice", "Hide the Danbooru account sign up notice."),
315 hide_tag_notice: newOption("checkbox", false, "Hide Tag Guide Notice", "Hide the Danbooru tag guide notice."),
316 hide_tos_notice: newOption("checkbox", false, "Hide TOS Notice", "Hide the Danbooru terms of service agreement notice."),
317 hide_upgrade_notice: newOption("checkbox", false, "Hide Upgrade Notice", "Hide the Danbooru upgrade account notice."),
318 hide_upload_notice: newOption("checkbox", false, "Hide Upload Guide Notice", "Hide the Danbooru upload guide notice."),
319 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 (Default):load", "View While Loading:view"]}),
320 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]}),
321 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."),
322 manage_cookies: newOption("checkbox", false, "Manage Notice Cookies", "When using the options to hide the upgrade, sign up, and/or TOS notice, 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."),
323 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."),
324 move_save_search: newOption("checkbox", false, "Move Save Search", "Move the \"save this search\" button into the related section in the sidebar."),
325 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"]}),
326 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."),
327 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."),
328 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."),
329 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."),
330 post_link_new_window: newOption("dropdown", "none", "New Tab/Window", "Force post links in the search, pool, popular, favorites, notes, 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"]}),
331 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."),
332 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 (Default):width", "Height:height", "Width & Height:all"]}),
333 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]}),
334 post_tag_titles: newOption("checkbox", false, "Post Tag Titles", "Change the page titles for posts to a full list of the post tags."),
335 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. <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. 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"]}),
336 remove_tag_headers: newOption("checkbox", false, "Remove Tag Headers", "Remove the \"copyrights\", \"characters\", and \"artist\" headers from the sidebar tag list."),
337 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"]}),
338 script_blacklisted_tags: "",
339 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"]}),
340 show_banned: newOption("checkbox", false, "Show Banned", "Display all banned posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
341 show_deleted: newOption("checkbox", false, "Show Deleted", "Display all deleted posts in the search, pool, popular, favorites, notes, 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."),
342 show_loli: newOption("checkbox", false, "Show Loli", "Display loli posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
343 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"]}),
344 show_shota: newOption("checkbox", false, "Show Shota", "Display shota posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
345 show_toddlercon: newOption("checkbox", false, "Show Toddlercon", "Display toddlercon posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
346 single_color_borders: newOption("checkbox", false, "Single Color Borders", "Only use one color for each thumbnail border."),
347 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"]}),
348 thumbnail_count: newOption("dropdown", 0, "Thumbnail Count", "Change the number of thumbnails that display in the search, favorites, and notes listings.", {txtOptions:["Disabled:0"], numRange:[1,200]}),
349 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."),
350 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"]),
351 tag_borders: borderSet(["loli", true, "#FFC0CB", "solid"], ["shota", true, "#66CCFF", "solid"], ["toddlercon", true, "#9370DB", "solid"], ["status:banned", true, "#000000", "solid"]),
352 thumb_cache_limit: newOption("dropdown", 5000, "Thumbnail Info Cache Limit", "Limit the number of thumbnail information entries cached in the browser.<tiphead>Note</tiphead>No actual thumbnails are cached. Only filename information used to speed up the display of hidden thumbnails is stored. Every 1000 entries is approximately equal to 0.1 megabytes of space.", {txtOptions:["Disabled:0"], numList:[1000,2000,3000,4000,5000,6000,7000,8000,9000,10000]}),
353 collapse_sidebar_data: {post: {}, thumb: {}},
354 track_new_data: {viewed: 0, viewing: 1}
355 },
356 quick_search: "",
357 search_add: {
358 active_links: {},
359 links: {},
360 old: ""
361 },
362 sections: { // Setting sections and ordering.
363 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"], "Options"),
364 border_options: newSection("general", ["custom_tag_borders", "custom_status_borders", "single_color_borders", "border_width", "border_spacing"], "Options"),
365 browse: newSection("general", ["show_loli", "show_shota", "show_toddlercon", "show_banned", "show_deleted", "thumbnail_count", "thumb_info", "post_link_new_window"], "Post Browsing"),
366 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"], "Post Control"),
367 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"),
368 notices: newSection("general", ["show_resized_notice", "minimize_status_notices", "hide_sign_up_notice", "hide_upgrade_notice", "hide_tos_notice", "hide_comment_notice", "hide_tag_notice", "hide_upload_notice", "hide_pool_notice", "hide_ban_notice"], "Notices"),
369 sidebar: newSection("general", ["remove_tag_headers", "post_tag_scrollbars", "search_tag_scrollbars", "autohide_sidebar", "fixed_sidebar", "collapse_sidebar"], "Tag Sidebar"),
370 misc: newSection("general", ["direct_downloads", "track_new", "clean_links", "arrow_nav", "post_tag_titles", "search_add", "page_counter", "comment_score", "quick_search"], "Misc."),
371 misc_layout: newSection("general", ["fixed_paginator", "move_save_search"], "Misc."),
372 script_settings: newSection("general", ["bypass_api", "manage_cookies", "enable_status_message", "resize_link_style", "override_blacklist", "override_resize", "override_sample", "thumb_cache_limit"], "Script Settings"),
373 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>."),
374 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>.")
375 },
376 settings: {
377 changed: {}
378 },
379 timers: {},
380 user: {} // User settings.
381 };
382
383 localStorageCheck();
384
385 loadSettings(); // Load user settings.
386
387 // Provide a session ID in order to detect XML requests carrying over from other pages.
388 window.bbbSession = new Date().getTime();
389
390 // Location variables.
391 var gLoc = danbLoc(); // Current location
392 var gLocRegex = new RegExp("\\b" + gLoc + "\\b");
393
394 // Script variables.
395 // Global
396 var show_loli = bbb.user.show_loli;
397 var show_shota = bbb.user.show_shota;
398 var show_toddlercon = bbb.user.show_toddlercon;
399 var show_banned = bbb.user.show_banned;
400 var show_deleted = bbb.user.show_deleted;
401 var direct_downloads = bbb.user.direct_downloads;
402 var post_link_new_window = bbb.user.post_link_new_window;
403
404 var blacklist_session_toggle = bbb.user.blacklist_session_toggle;
405 var blacklist_post_display = bbb.user.blacklist_post_display;
406 var blacklist_thumb_mark = bbb.user.blacklist_thumb_mark;
407 var blacklist_highlight_color = bbb.user.blacklist_highlight_color;
408 var blacklist_add_bars = bbb.user.blacklist_add_bars;
409 var blacklist_thumb_controls = bbb.user.blacklist_thumb_controls;
410 var blacklist_smart_view = bbb.user.blacklist_smart_view;
411
412 var custom_tag_borders = bbb.user.custom_tag_borders;
413 var custom_status_borders = bbb.user.custom_status_borders;
414 var single_color_borders = bbb.user.single_color_borders;
415 var border_spacing = bbb.user.border_spacing;
416 var border_width = bbb.user.border_width;
417 var clean_links = bbb.user.clean_links;
418 var comment_score = bbb.user.comment_score;
419 var thumb_info = bbb.user.thumb_info;
420 var autohide_sidebar = gLocRegex.test(bbb.user.autohide_sidebar);
421 var fixed_sidebar = gLocRegex.test(bbb.user.fixed_sidebar);
422 var fixed_paginator = bbb.user.fixed_paginator;
423 var collapse_sidebar = bbb.user.collapse_sidebar;
424 var move_save_search = bbb.user.move_save_search;
425 var page_counter = bbb.user.page_counter;
426 var quick_search = bbb.user.quick_search;
427
428 var bypass_api = bbb.user.bypass_api;
429 var manage_cookies = bbb.user.manage_cookies;
430 var enable_status_message = bbb.user.enable_status_message;
431 var resize_link_style = bbb.user.resize_link_style;
432 var override_blacklist = bbb.user.override_blacklist;
433 var override_resize = bbb.user.override_resize;
434 var override_sample = bbb.user.override_sample;
435 var track_new = bbb.user.track_new;
436
437 var show_resized_notice = bbb.user.show_resized_notice;
438 var hide_sign_up_notice = bbb.user.hide_sign_up_notice;
439 var hide_upgrade_notice = bbb.user.hide_upgrade_notice;
440 var minimize_status_notices = bbb.user.minimize_status_notices;
441 var hide_tos_notice = bbb.user.hide_tos_notice;
442 var hide_comment_notice = bbb.user.hide_comment_notice;
443 var hide_tag_notice = bbb.user.hide_tag_notice;
444 var hide_upload_notice = bbb.user.hide_upload_notice;
445 var hide_pool_notice = bbb.user.hide_pool_notice;
446 var hide_ban_notice = bbb.user.hide_ban_notice;
447
448 // Search
449 var arrow_nav = bbb.user.arrow_nav;
450 var search_add = bbb.user.search_add;
451 var search_tag_scrollbars = bbb.user.search_tag_scrollbars;
452 var thumbnail_count = bbb.user.thumbnail_count;
453 var thumbnail_count_default = 20; // Number of thumbnails BBB should expect Danbooru to return by default.
454 var thumb_cache_limit = bbb.user.thumb_cache_limit;
455
456 // Post
457 var alternate_image_swap = bbb.user.alternate_image_swap;
458 var post_resize = accountSettingCheck("post_resize");
459 var post_resize_mode = bbb.user.post_resize_mode;
460 var post_drag_scroll = bbb.user.post_drag_scroll;
461 var load_sample_first = accountSettingCheck("load_sample_first");
462 var remove_tag_headers = bbb.user.remove_tag_headers;
463 var post_tag_scrollbars = bbb.user.post_tag_scrollbars;
464 var post_tag_titles = bbb.user.post_tag_titles;
465 var autoscroll_post = bbb.user.autoscroll_post;
466 var image_swap_mode = bbb.user.image_swap_mode;
467 var disable_embedded_notes = bbb.user.disable_embedded_notes;
468
469 // Endless
470 var endless_default = bbb.user.endless_default;
471 var endless_fill = bbb.user.endless_fill;
472 var endless_pause_interval = bbb.user.endless_pause_interval;
473 var endless_preload = bbb.user.endless_preload;
474 var endless_remove_dup = bbb.user.endless_remove_dup;
475 var endless_scroll_limit = bbb.user.endless_scroll_limit;
476 var endless_separator = bbb.user.endless_separator;
477 var endless_session_toggle = bbb.user.endless_session_toggle;
478
479 // Stored data
480 var status_borders = bbb.user.status_borders;
481 var tag_borders = bbb.user.tag_borders;
482 var collapse_sidebar_data = bbb.user.collapse_sidebar_data;
483 var track_new_data = bbb.user.track_new_data;
484 var script_blacklisted_tags = bbb.user.script_blacklisted_tags;
485
486 // Other data
487 var bbbHiddenImg = "data:image/png;base64,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";
488 var bbbBlacklistImg = "data:image/png;base64,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";
489 var bbbBlacklistIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAAkFBMVEUAAAD////////////////////////////////////////////////////////////////p6en////////////////////////////////////////////////////////+/v7///////////////////////////////////////////////////////////97JICZAAAAL3RSTlMACAQBoUSi5QcnMfmnsMW9VQno+vjsxrwstPyzMhOpSxS65vX06czqQf7NvqbzQKZY7GsAAADASURBVHhefZDnDoJAEITn8Kh3FAEFQbH3Mu//doaNLSFxfn7J7hT80cgJlAqc0S8bu55P+p47/rJQ8yUdflhCHq/WpjmZvKjSPPOyBFbZlNRKPFySzTaOANQJ6fZujsf9YUcjNMvpOQACn+kyLmjaObBI6QcAFGkRxYblLAIsqd4Q0ayUDzeBcr4A5q2h6U5Vfy5GeQbIh8l6I0YSKal72k3uhUSS8OQ0WwGPddFQq2/NPLW22rxrDgcZTvd35KGevk8VfmeGhUQAAAAASUVORK5CYII=";
490
491 /* "INIT" */
492 modifyDanbScript();
493
494 customCSS(); // Contains the portions related to notices.
495
496 delayMe(formatThumbnails); // Delayed to allow Danbooru to run first.
497
498 delayMe(blacklistInit); // Delayed to allow Danbooru to run first.
499
500 thumbInfo();
501
502 removeTagHeaders();
503
504 searchAdd();
505
506 minimizeStatusNotices();
507
508 postTagTitles();
509
510 trackNew();
511
512 injectSettings();
513
514 modifyPage();
515
516 autohideSidebar();
517
518 delayMe(fixedSidebar); // Delayed to allow Danbooru layout to finalize.
519
520 delayMe(collapseSidebar); // Delayed to allow Danbooru layout to finalize.
521
522 delayMe(fixedPaginator); // Delayed to allow Danbooru layout to finalize.
523
524 moveSaveSearch();
525
526 pageCounter();
527
528 quickSearch();
529
530 postLinkNewWindow();
531
532 commentScoreInit();
533
534 cleanLinks();
535
536 postDDL();
537
538 arrowNav();
539
540 fixLimit();
541
542 bbbHotkeys();
543
544 endlessInit();
545
546 /* Functions */
547
548 /* Functions for XML API info */
549 function searchJSON(mode, optArg) {
550 // Figure out the desired URL for a JSON API request, trigger any necessary xml flag, and update the status message.
551 var url = location.href.split("#", 1)[0];
552 var idCache;
553 var idList;
554 var idSearch;
555 var page;
556
557 if (mode === "search" || mode === "notes" || mode === "favorites") {
558 if (potentialHiddenPosts(mode)) {
559 url = (allowUserLimit() ? updateURLQuery(url, {limit: thumbnail_count}) : url);
560 bbb.flags.thumbs_xml = true;
561
562 if (mode === "search")
563 fetchJSON(url.replace(/\/?(?:posts)?\/?(?:\?|$)/, "/posts.json?"), "search");
564 else if (mode === "notes")
565 fetchJSON(url.replace(/\/notes\/?(?:\?|$)/, "/notes.json?"), "notes");
566 else if (mode === "favorites")
567 fetchJSON(url.replace(/\/favorites\/?(?:\?|$)/, "/favorites.json?"), "favorites");
568
569 bbbStatus("posts", "new");
570 }
571 }
572 else if (mode === "popular" || mode === "popular_view") {
573 if (potentialHiddenPosts(mode)) {
574 bbb.flags.thumbs_xml = true;
575
576 fetchJSON(url.replace(/\/(popular_view|popular)\/?/, "/$1.json"), mode);
577 bbbStatus("posts", "new");
578 }
579 }
580 else if (mode === "pool" || mode === "favorite_group") {
581 if (potentialHiddenPosts(mode)) {
582 idCache = getIdCache();
583 bbb.flags.thumbs_xml = true;
584
585 if (idCache)
586 searchJSON(mode + "_search", {post_ids: idCache});
587 else // Get a new cache.
588 fetchJSON(url.replace(/\/(pools|favorite_groups)\/(\d+)/, "/$1/$2.json"), mode + "_cache", mode + "_search");
589
590 bbbStatus("posts", "new");
591 }
592 }
593 else if (mode === "pool_search" || mode === "favorite_group_search") {
594 page = Number(getVar("page")) || 1;
595 idList = optArg.post_ids.split(" ");
596 idSearch = idList.slice((page - 1) * thumbnail_count_default, page * thumbnail_count_default);
597
598 fetchJSON("/posts.json?tags=status:any+id:" + idSearch.join(","), mode, idSearch);
599 }
600 else if (mode === "endless") {
601 bbb.flags.endless_xml = true;
602
603 if (gLoc === "pool" || gLoc === "favorite_group") {
604 idCache = getIdCache();
605
606 if (idCache)
607 searchJSON("endless_" + gLoc + "_search", {post_ids: idCache});
608 else // Get a new cache.
609 fetchJSON(url.replace(/\/(pools|favorite_groups)\/(\d+)/, "/$1/$2.json"), gLoc + "_cache", "endless_" + gLoc + "_search");
610 }
611 else {
612 url = endlessNexURL();
613
614 fetchJSON(url.replace(/(\?)|$/, ".json$1"), "endless");
615 }
616
617 bbbStatus("posts", "new");
618 }
619 else if (mode === "endless_pool_search" || mode === "endless_favorite_group_search") {
620 idList = optArg.post_ids.split(" ");
621 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.
622 idSearch = idList.slice((page - 1) * thumbnail_count_default, page * thumbnail_count_default);
623
624 fetchJSON("/posts.json?tags=status:any+id:" + idSearch.join(","), "endless", idSearch);
625 }
626 else if (mode === "comments") {
627 if (potentialHiddenPosts(mode)) {
628 fetchJSON(url.replace(/\/comments\/?/, "/comments.json"), "comments");
629 bbbStatus("posts", "new");
630 }
631 }
632 else if (mode === "parent" || mode === "child") {
633 var parentUrl = "/posts.json?limit=200&tags=status:any+parent:" + optArg;
634
635 fetchJSON(parentUrl, mode, optArg);
636 bbbStatus("posts", "new");
637 }
638 else if (mode === "ugoira") {
639 fetchJSON(url.replace(/\/posts\/(\d+)/, "/posts/$1.json"), "ugoira");
640 bbbStatus("posts", "new");
641 }
642 }
643
644 function fetchJSON(url, mode, optArg, session, retries) {
645 // Retrieve JSON.
646 var xmlhttp = new XMLHttpRequest();
647 var xmlRetries = retries || 0;
648 var xmlSession = session || window.bbbSession;
649
650 if (xmlhttp !== null) {
651 xmlhttp.onreadystatechange = function() {
652 if (xmlSession !== window.bbbSession) // If we end up receiving an xml response from a different page, reject it.
653 xmlhttp.abort();
654 else if (xmlhttp.readyState === 4) { // 4 = "loaded"
655 if (xmlhttp.status === 200) { // 200 = "OK"
656 var xml = JSON.parse(xmlhttp.responseText);
657
658 // Update status message.
659 if (mode === "search" || mode === "popular" || mode === "popular_view" || mode === "notes" || mode === "favorites" || mode === "pool_search" || mode === "favorite_group_search") {
660 bbb.flags.thumbs_xml = false;
661
662 parseListing(xml, optArg);
663 }
664 else if (mode === "post")
665 parsePost(xml);
666 else if (mode === "pool_cache" || mode === "favorite_group_cache") {
667 var collId = /\/(?:pools|favorite_groups)\/(\d+)/.exec(location.href)[1];
668
669 sessionStorage.bbbSetItem("bbb_" + mode + "_" + collId, new Date().getTime() + " " + xml.post_ids);
670 searchJSON(optArg, xml);
671 }
672 else if (mode === "endless") {
673 bbb.flags.endless_xml = false;
674
675 endlessXMLJSONHandler(xml, optArg);
676 }
677 else if (mode === "comments")
678 parseComments(xml);
679 else if (mode === "parent" || mode === "child")
680 parseRelations(xml, mode, optArg);
681 else if (mode === "ugoira")
682 fixHiddenUgoira(xml);
683
684 if (mode !== "pool_cache" && mode !== "favorite_group_cache")
685 bbbStatus("posts", "done");
686 }
687 else {
688 if (xmlhttp.status === 403 || xmlhttp.status === 401) {
689 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);
690 document.getElementById("bbb-bypass-api-link").addEventListener("click", function(event) {
691 updateSettings("bypass_api", true);
692 this.parentNode.innerHTML = "Settings updated. You may change this setting under the preferences tab in the settings panel.";
693 event.preventDefault();
694 }, false);
695 bbbStatus("posts", "error");
696 }
697 else if (xmlhttp.status === 421) {
698 bbbNotice("Error retrieving post information. Your Danbooru API access is currently throttled. Please try again later.", -1);
699 bbbStatus("posts", "error");
700 }
701 else if (xmlhttp.status !== 0) {
702 if (xmlRetries < 1) {
703 xmlRetries++;
704 fetchJSON(url, mode, optArg, xmlSession, xmlRetries);
705 }
706 else {
707 var linkId = uniqueIdNum(); // Create a unique ID.
708 var noticeMsg = bbbNotice('Error retrieving post information (JSON Code: ' + xmlhttp.status + ' ' + xmlhttp.statusText + '). (<a id="' + linkId + '" href="#">Retry</a>)', -1);
709
710 bbbStatus("posts", "error");
711
712 document.getElementById(linkId).addEventListener("click", function(event) {
713 closeBbbNoticeMsg(noticeMsg);
714 searchJSON(mode, optArg);
715 event.preventDefault();
716 }, false);
717 }
718 }
719 }
720 }
721 };
722 xmlhttp.open("GET", url, true);
723 xmlhttp.send(null);
724 }
725 }
726
727 function parseListing(xml, optArg) {
728 // Use JSON results for thumbnail listings.
729 var posts = xml;
730 var thumbContainer = getThumbContainer(gLoc);
731 var orderedIds = (gLoc === "pool" || gLoc === "favorite_group" ? optArg : undefined);
732 var before = getThumbSibling(gLoc);
733
734 if (!posts[0])
735 return;
736
737 if (!thumbContainer) {
738 bbbNotice("Thumbnail section could not be located.", -1);
739 return;
740 }
741
742 // Thumb preparation.
743 var newThumbs = createThumbListing(posts, orderedIds);
744
745 // New thumbnail container preparation.
746 var replacement = thumbContainer.cloneNode(false);
747 var childIndex = 0;
748
749 while (thumbContainer.children[childIndex]) {
750 var child = thumbContainer.children[childIndex];
751
752 if (child.tagName !== "ARTICLE")
753 replacement.appendChild(child);
754 else
755 childIndex++;
756 }
757
758 if (!before)
759 replacement.appendChild(newThumbs);
760 else
761 replacement.insertBefore(newThumbs, before);
762
763 // Prepare thumbnails.
764 prepThumbnails(replacement);
765
766 // Replace results with new results.
767 thumbContainer.parentNode.replaceChild(replacement, thumbContainer);
768
769 // Fix the paginator. The paginator isn't always in the replacement, so run this on the whole page after the replacement is inserted.
770 fixPaginator();
771
772 // Fix hidden thumbnails.
773 fixHiddenThumbs();
774
775 // Update the URL with the limit value.
776 if (allowUserLimit())
777 history.replaceState((history.state || {}), "", updateURLQuery(location.search, {limit: thumbnail_count}));
778 }
779
780 function parsePost(postInfo) {
781 // Take a post's info and alter its page.
782 var post = bbb.post.info = formatInfo(postInfo || scrapePost());
783 var imgContainer = document.getElementById("image-container");
784
785 if (!imgContainer) {
786 bbbNotice("Post content could not be located.", -1);
787 return;
788 }
789
790 if (!post || !post.file_url) {
791 bbbNotice("Due to a lack of provided information, this post cannot be viewed.", -1);
792 return;
793 }
794
795 // Stop if we're on Safebooru and the image isn't safe.
796 if (safebPostTest(post))
797 return;
798
799 // Enable the "Resize to window", "Toggle Notes", "Random Post", and "Find similar" options for logged out users.
800 createOptionsSection();
801
802 // Fix the direct post links in the information and options sections for hidden posts.
803 fixPostDownloadLinks();
804
805 // Replace the "resize to window" link with new resize links.
806 modifyResizeLink();
807
808 // Keep any original video from continuing to play/download after being removed.
809 var origVideo = imgContainer.getElementsByTagName("video")[0];
810
811 if (origVideo) {
812 origVideo.pause();
813 origVideo.src = "about:blank";
814 origVideo.load();
815 }
816
817 // Create content.
818 if (post.file_ext === "swf") // Create flash object.
819 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <object height="' + post.image_height + '" width="' + post.image_width + '"> <params name="movie" value="' + post.file_url + '"> <embed allowscriptaccess="never" src="' + post.file_url + '" height="' + post.image_height + '" width="' + post.image_width + '"> </params> </object> <p><a href="' + post.file_url + '">Save this flash (right click and save)</a></p>';
820 else if (post.file_ext === "webm" || post.file_ext === "mp4") { // Create video
821 var playerLoop = (post.has_sound ? '' : ' loop="loop"'); // No looping for videos with sound.
822
823 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <video id="image" autoplay="autoplay"' + playerLoop + ' controls="controls" src="' + post.file_url + '" height="' + post.image_height + '" width="' + post.image_width + '"></video> <p><a href="' + post.file_url + '">Save this video (right click and save)</a></p>';
824 }
825 else if (post.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(post.tag_string)) { // Create ugoira
826 var useUgoiraOrig = getVar("original");
827
828 // Get rid of all the old events handlers.
829 if (Danbooru.Ugoira && Danbooru.Ugoira.player)
830 $(Danbooru.Ugoira.player).unbind();
831
832 if ((load_sample_first && useUgoiraOrig !== "1") || useUgoiraOrig === "0") { // Load sample webm version.
833 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <video id="image" autoplay="autoplay" loop="loop" controls="controls" src="' + post.large_file_url + '" height="' + post.image_height + '" width="' + post.image_width + '" data-fav-count="' + post.fav_count + '" data-flags="' + post.flags + '" data-has-active-children="' + post.has_active_children + '" data-has-children="' + post.has_children + '" data-large-height="' + post.sample_height + '" data-large-width="' + post.sample_width + '" data-original-height="' + post.image_height + '" data-original-width="' + post.image_width + '" data-rating="' + post.rating + '" data-score="' + post.score + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '"></video> <p><a href="' + post.large_file_url + '">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>';
834
835 // Prep the "toggle notes" link.
836 noteToggleLinkInit();
837 }
838 else { // Load original ugoira version.
839 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <canvas data-ugoira-content-type="' + post.pixiv_ugoira_frame_data.content_type.replace(/"/g, """) + '" data-ugoira-frames="' + JSON.stringify(post.pixiv_ugoira_frame_data.data).replace(/"/g, """) + '" data-fav-count="' + post.fav_count + '" data-flags="' + post.flags + '" data-has-active-children="' + post.has_active_children + '" data-has-children="' + post.has_children + '" data-large-height="' + post.image_height + '" data-large-width="' + post.image_width + '" data-original-height="' + post.image_height + '" data-original-width="' + post.image_width + '" data-rating="' + post.rating + '" data-score="' + post.score + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" height="' + post.image_height + '" width="' + post.image_width + '" id="image"></canvas> <div id="ugoira-controls"> <div id="ugoira-control-panel" style="width: ' + post.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: ' + (post.image_width - 81) + 'px; min-width: 269px;"></div> </div> <p id="save-video-link"><a href="' + post.large_file_url + '">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>';
840
841 // Make notes toggle when clicking the ugoira animation.
842 noteToggleInit();
843
844 // Prep the "toggle notes" link. The "toggle notes" link is added here just for consistency's sake.
845 noteToggleLinkInit();
846
847 if (post.pixiv_ugoira_frame_data.data) // Set up the post.
848 ugoiraInit();
849 else // Fix hidden posts.
850 searchJSON("ugoira");
851 }
852 }
853 else if (!post.image_height) // Create manual download.
854 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div><p><a href="' + post.file_url + '">Save this file (right click and save)</a></p>';
855 else { // Create image
856 var newWidth = 0;
857 var newHeight = 0;
858 var newUrl = "";
859 var altTxt = "";
860
861 if (load_sample_first && post.has_large) {
862 newWidth = post.sample_width;
863 newHeight = post.sample_height;
864 newUrl = post.large_file_url;
865 altTxt = "Sample";
866 }
867 else {
868 newWidth = post.image_width;
869 newHeight = post.image_height;
870 newUrl = post.file_url;
871 altTxt = post.md5;
872 }
873
874 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <img alt="' + altTxt + '" data-fav-count="' + post.fav_count + '" data-flags="' + post.flags + '" data-has-active-children="' + post.has_active_children + '" data-has-children="' + post.has_children + '" data-large-height="' + post.sample_height + '" data-large-width="' + post.sample_width + '" data-original-height="' + post.image_height + '" data-original-width="' + post.image_width + '" data-rating="' + post.rating + '" data-score="' + post.score + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.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;"/>';
875
876 bbb.el.bbbLoader = document.getElementById("bbb-loader");
877
878 // Create/replace the elements related to image swapping and set them up.
879 swapImageInit();
880
881 if (alternate_image_swap) // Make sample/original images swap when clicking the image.
882 alternateImageSwap();
883 else // Make notes toggle when clicking the image.
884 noteToggleInit();
885 }
886
887 // Enable drag scrolling.
888 dragScrollInit();
889
890 // Resize the content if desired.
891 if (post_resize)
892 resizePost(post_resize_mode);
893
894 // Enable translation mode.
895 translationModeInit();
896
897 // Disable embedded notes.
898 disableEmbeddedNotes();
899
900 // Load/reload notes.
901 Danbooru.Note.load_all("bbb");
902
903 // Auto position the content if desired.
904 autoscrollPost();
905
906 // Blacklist.
907 blacklistUpdate();
908
909 // Fix the parent/child notice(s).
910 checkRelations();
911 }
912
913 function parseComments(xml) {
914 // Fix missing comments by inserting them into their appropriate position.
915 var posts = xml;
916 var numPosts = posts.length;
917 var expectedPosts = numPosts;
918 var existingPosts = getPosts(); // Live node list so adding/removing a "post-preview" class item immediately changes this.
919 var eci = 0;
920
921 for (var i = 0; i < numPosts; i++) {
922 var post = formatInfo(posts[i]);
923 var existingPost = existingPosts[eci];
924
925 if (!existingPost || String(post.id) !== existingPost.getAttribute("data-id")) {
926 if (!/(?:^|\s)(?:loli|shota|toddlercon)(?:$|\s)/.test(post.tag_string) && !post.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.
927 continue;
928 else 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_banned && post.is_banned) || safebPostTest(post)) { // Skip hidden posts if the user has selected to do so.
929 expectedPosts--;
930 continue;
931 }
932
933 // Prepare the post information.
934 var tagLinks = post.tag_string.bbbSpacePad();
935 var generalTags = post.tag_string_general.split(" ");
936 var artistTags = post.tag_string_artist.split(" ");
937 var copyrightTags = post.tag_string_copyright.split(" ");
938 var characterTags = post.tag_string_character.split(" ");
939 var limit = (thumbnail_count ? "&limit=" + thumbnail_count : "");
940 var tag;
941 var j, jl; // Loop variables.
942
943 for (j = 0, jl = generalTags.length; j < jl; j++) {
944 tag = generalTags[j];
945 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-0"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
946 }
947
948 for (j = 0, jl = artistTags.length; j < jl; j++) {
949 tag = artistTags[j];
950 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-1"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
951 }
952
953 for (j = 0, jl = copyrightTags.length; j < jl; j++) {
954 tag = copyrightTags[j];
955 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-3"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
956 }
957
958 for (j = 0, jl = characterTags.length; j < jl; j++) {
959 tag = characterTags[j];
960 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-4"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
961 }
962
963 // Create the new post.
964 var childSpan = document.createElement("span");
965
966 childSpan.innerHTML = '<div id="post_' + post.id + '" class="post post-preview' + post.thumb_class + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" data-rating="' + post.rating + '" data-flags="' + post.flags + '" data-score="' + post.score + '" data-parent-id="' + post.parent_id + '" data-has-children="' + post.has_children + '" data-id="' + post.id + '" data-has-sound="' + post.has_sound + '" data-width="' + post.image_width + '" data-height="' + post.image_height + '" data-approver-id="' + post.approver_id + '" data-fav-count="' + post.fav_count + '" data-pixiv-id="' + post.pixiv_id + '" data-md5="' + post.md5 + '" data-file-ext="' + post.file_ext + '" data-file-url="' + post.file_url + '" data-large-file-url="' + post.large_file_url + '" data-preview-file-url="' + post.preview_file_url + '"> <div class="preview"> <a href="/posts/' + post.id + '"> <img alt="' + post.md5 + '" src="' + post.preview_file_url + '" /> </a> </div> <div class="comments-for-post" data-post-id="' + post.id + '"> <div class="header"> <div class="row"> <span class="info"> <strong>Date</strong> <time datetime="' + post.created_at + '" title="' + post.created_at.replace(/(.+)T(.+)-(.+)/, "$1 $2 -$3") + '">' + post.created_at.replace(/(.+)T(.+):\d+-.+/, "$1 $2") + '</time> </span> <span class="info"> <strong>User</strong> <a href="/users/' + post.uploader_id + '">' + post.uploader_name + '</a> </span> <span class="info"> <strong>Rating</strong> ' + post.rating + ' </span> <span class="info"> <strong>Score</strong> <span> <span id="score-for-post-' + post.id + '">' + post.score + '</span> </span> </span> </div> <div class="row list-of-tags"> <strong>Tags</strong>' + tagLinks + '</div> </div> </div> <div class="clearfix"></div> </div>';
967
968 // Prepare thumbnails.
969 prepThumbnails(childSpan);
970
971 if (!existingPost) // There isn't a next post so append the new post to the end before the paginator.
972 document.getElementById("a-index").insertBefore(childSpan.firstElementChild, getPaginator());
973 else // Insert new post before the post that should follow it.
974 existingPost.parentNode.insertBefore(childSpan.firstElementChild, existingPost);
975
976 // Get the comments and image info.
977 searchPages("post_comments", post.id);
978 }
979
980 eci++;
981 }
982
983 // 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.)
984 // if (existingPosts.length !== expectedPosts)
985 // bbbNotice("Loading of hidden post(s) failed. Please refresh.", -1);
986 }
987
988 function parseRelations(xml, mode, parentId) {
989 // Create a new parent/child notice.
990 var posts = xml;
991 var post;
992 var activePost = bbb.post.info;
993 var numPosts = posts.length;
994 var relationCookie = getCookie()["show-relationship-previews"];
995 var showPreview = (relationCookie === undefined || relationCookie === "1" ? true : false);
996 var childSpan = document.createElement("span");
997 var target;
998 var previewLinkId;
999 var previewLinkTxt;
1000 var previewId;
1001 var classes;
1002 var msg;
1003 var query = "?tags=parent:" + parentId + (show_deleted ? "+status:any" : "") + (thumbnail_count ? "&limit=" + thumbnail_count : "");
1004 var thumbs = "";
1005 var displayStyle;
1006 var forceShowDeleted = activePost.is_deleted; // If the parent is deleted or the active post is deleted, all deleted posts are shown.
1007 var parentDeleted = false;
1008 var isSafebooru = (location.host.indexOf("safebooru") > -1 ? true : false);
1009 var i; // Loop variable.
1010
1011 // Figure out if the parent is deleted.
1012 for (i = 0; i < numPosts; i++) {
1013 post = posts[i];
1014
1015 if (post.id === parentId) {
1016 parentDeleted = post.is_deleted;
1017 forceShowDeleted = forceShowDeleted || parentDeleted;
1018 }
1019 }
1020
1021 // Set up the notice variables.
1022 if (showPreview) {
1023 previewLinkTxt = "« hide";
1024 displayStyle = "block";
1025 }
1026 else {
1027 previewLinkTxt = "show »";
1028 displayStyle = "none";
1029 }
1030
1031 if (mode === "child") {
1032 target = document.getElementsByClassName("notice-child")[0];
1033 previewLinkId = "has-parent-relationship-preview-link";
1034 previewId = "has-parent-relationship-preview";
1035 classes = "notice-child";
1036
1037 if (!isSafebooru) {
1038 if (numPosts)
1039 msg = 'This post belongs to a <a href="/posts' + query + '">parent</a>' + (parentDeleted ? " (deleted)" : "");
1040
1041 if (numPosts === 3)
1042 msg += ' and has <a href="/posts' + query + '">a sibling</a>';
1043 else if (numPosts > 3)
1044 msg += ' and has <a href="/posts' + query + '">' + (numPosts - 2) + ' siblings</a>';
1045 }
1046 else {
1047 var parentNotSafe = true;
1048
1049 for (i = 0; i < numPosts; i++) {
1050 if (posts[i].id === parentId)
1051 parentNotSafe = false;
1052 }
1053
1054 var siblingLimit = (parentNotSafe ? 2 : 3);
1055
1056 if (numPosts)
1057 msg = 'This post belongs to a <a href="/posts' + query + '">parent</a>' + (parentNotSafe ? " (not safe)" : (parentDeleted ? " (deleted)" : ""));
1058
1059 if (numPosts === siblingLimit)
1060 msg += ' and has <a href="/posts' + query + '">a sibling</a>';
1061 else if (numPosts > siblingLimit)
1062 msg += ' and has <a href="/posts' + query + '">' + (numPosts - 2) + ' siblings</a>';
1063 }
1064 }
1065 else if (mode === "parent") {
1066 target = document.getElementsByClassName("notice-parent")[0];
1067 previewLinkId = "has-children-relationship-preview-link";
1068 previewId = "has-children-relationship-preview";
1069 classes = "notice-parent";
1070
1071 if (!isSafebooru) {
1072 if (numPosts === 2)
1073 msg = 'This post has <a href="/posts' + query + '">a child</a>';
1074 else if (numPosts > 2)
1075 msg = 'This post has <a href="/posts' + query + '">' + (numPosts - 1) + ' children</a>';
1076 }
1077 else {
1078 if (numPosts === 1)
1079 msg = 'This post has no safe <a href="/posts' + query + '">children</a>';
1080 else if (numPosts === 2)
1081 msg = 'This post has <a href="/posts' + query + '">a child</a>';
1082 else if (numPosts > 2)
1083 msg = 'This post has <a href="/posts' + query + '">' + (numPosts - 1) + ' children</a>';
1084 }
1085 }
1086
1087 // Create the main notice element.
1088 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>';
1089
1090 var newNotice = childSpan.firstElementChild;
1091 var thumbDiv = getId(previewId, newNotice);
1092 var previewLink = getId(previewLinkId, newNotice);
1093
1094 // Create the thumbnails.
1095 for (i = numPosts - 1; i >= 0; i--) {
1096 post = formatInfo(posts[i]);
1097
1098 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))
1099 continue;
1100
1101 checkHiddenThumbs(post);
1102
1103 var thumb = createThumbHTML(post, (clean_links ? "" : query)) + " ";
1104
1105 if (post.id === parentId)
1106 thumbs = thumb + thumbs;
1107 else
1108 thumbs += thumb;
1109 }
1110
1111 thumbDiv.innerHTML = thumbs;
1112
1113 // Highlight the post we're on.
1114 var activeThumb = getId("post_" + activePost.id, thumbDiv);
1115
1116 if (activeThumb)
1117 activeThumb.bbbAddClass("current-post");
1118
1119 // Make the show/hide links work.
1120 newNotice.addEventListener("click", function(event) {
1121 if (event.target !== previewLink)
1122 return;
1123
1124 if (thumbDiv.style.display === "block") {
1125 thumbDiv.style.display = "none";
1126 previewLink.innerHTML = "show »";
1127 createCookie("show-relationship-previews", 0, 365);
1128 }
1129 else {
1130 thumbDiv.style.display = "block";
1131 previewLink.innerHTML = "« hide";
1132 createCookie("show-relationship-previews", 1, 365);
1133 }
1134
1135 event.preventDefault();
1136 event.stopPropagation();
1137 }, true);
1138
1139 // Prepare thumbnails.
1140 prepThumbnails(newNotice);
1141
1142 // Replace/add the notice.
1143 if (target)
1144 target.parentNode.replaceChild(newNotice, target);
1145 else if (mode === "child") {
1146 target = document.getElementsByClassName("notice-parent")[0] || bbb.el.resizeNotice || document.getElementById("image-container");
1147 target.parentNode.insertBefore(newNotice, target);
1148 }
1149 else if (mode === "parent") {
1150 target = bbb.el.resizeNotice || document.getElementById("image-container");
1151 target.parentNode.insertBefore(newNotice, target);
1152 }
1153
1154 // Fix hidden thumbnails.
1155 fixHiddenThumbs();
1156 }
1157
1158 function fixHiddenUgoira(xml) {
1159 // Use xml info to fix the missing info for hidden ugoira posts.
1160 var post = bbb.post.info;
1161 post.pixiv_ugoira_frame_data = xml.pixiv_ugoira_frame_data;
1162
1163 var imgContainer = document.getElementById("image-container");
1164 var ugoira = (imgContainer ? imgContainer.getElementsByTagName("canvas")[0] : undefined);
1165
1166 if (ugoira) {
1167 // Fix the missing data attributes.
1168 ugoira.setAttribute("data-ugoira-content-type", post.pixiv_ugoira_frame_data.content_type);
1169 ugoira.setAttribute("data-ugoira-frames", JSON.stringify(post.pixiv_ugoira_frame_data.data));
1170
1171 // Append the necessary script.
1172 var mainScript = document.createElement("script");
1173 mainScript.addEventListener("load", ugoiraInit, true); // Wait for this script to load before running the JavaScript that requires it.
1174 mainScript.src = "/assets/ugoira_player.js";
1175 document.head.appendChild(mainScript);
1176 }
1177 }
1178
1179 function endlessXMLJSONHandler(xml, optArg) {
1180 // Create a thumbnail listing from JSON results and pass it to the queue.
1181 var orderedIds = optArg;
1182 var posts = createThumbListing(xml, orderedIds);
1183 var newPage = document.createElement("div");
1184 newPage.className = "bbb-endless-page";
1185
1186 newPage.appendChild(posts);
1187 endlessQueuePage(newPage);
1188 }
1189
1190 /* Functions for XML page info */
1191 function searchPages(mode, optArg) {
1192 // Let other functions that don't require the API run (alternative to searchJSON) and retrieve various pages for info.
1193 var url;
1194
1195 if (mode === "search" || mode === "notes" || mode === "favorites" || mode === "thumbnails") {
1196 if (allowUserLimit()) {
1197 url = updateURLQuery(location.href, {limit: thumbnail_count});
1198 bbb.flags.thumbs_xml = true;
1199
1200 fetchPages(url, "thumbnails");
1201 bbbStatus("posts", "new");
1202 }
1203 }
1204 else if (mode === "endless") {
1205 url = endlessNexURL();
1206 bbb.flags.endless_xml = true;
1207
1208 fetchPages(url, "endless");
1209 bbbStatus("posts", "new");
1210 }
1211 else if (mode === "paginator") {
1212 url = (allowUserLimit() ? updateURLQuery(location.href, {limit: thumbnail_count}) : location.href);
1213 bbb.flags.paginator_xml = true;
1214
1215 fetchPages(url, "paginator");
1216 }
1217 else if (mode === "post_comments") {
1218 url = "/posts/" + optArg;
1219
1220 fetchPages(url, "post_comments", optArg);
1221 bbbStatus("post_comments", "new");
1222 }
1223 else if (mode === "hidden") {
1224 url = "/posts/" + optArg;
1225 bbb.flags.hidden_xml = true;
1226
1227 fetchPages(url, "hidden", optArg);
1228 bbbStatus("hidden", "new");
1229 }
1230 }
1231
1232 function fetchPages(url, mode, optArg, session, retries) {
1233 // Retrieve an actual page for certain pieces of information.
1234 var xmlhttp = new XMLHttpRequest();
1235 var xmlRetries = retries || 0;
1236 var xmlSession = session || window.bbbSession;
1237
1238 if (xmlhttp !== null) {
1239 xmlhttp.onreadystatechange = function() {
1240 if (xmlSession !== window.bbbSession) // If we end up receiving an xml response form a different page, reject it.
1241 xmlhttp.abort();
1242 else if (xmlhttp.readyState === 4) { // 4 = "loaded"
1243 if (xmlhttp.status === 200) { // 200 = "OK"
1244 var docEl = document.createElement("html");
1245
1246 docEl.innerHTML = xmlhttp.responseText;
1247
1248 if (mode === "paginator") {
1249 bbb.flags.paginator_xml = false;
1250
1251 replacePaginator(docEl);
1252 }
1253 else if (mode === "post_comments") {
1254 replaceComments(docEl, optArg);
1255 bbbStatus("post_comments", "done");
1256 }
1257 else if (mode === "thumbnails") {
1258 bbb.flags.thumbs_xml = false;
1259
1260 replaceThumbnails(docEl);
1261 bbbStatus("posts", "done");
1262 }
1263 else if (mode === "hidden") {
1264 bbb.flags.hidden_xml = false;
1265
1266 replaceHidden(docEl);
1267 bbbStatus("hidden", "done");
1268 }
1269 else if (mode === "endless") {
1270 bbb.flags.endless_xml = false;
1271
1272 endlessXMLPageHandler(docEl);
1273 bbbStatus("posts", "done");
1274 }
1275 }
1276 else if (xmlhttp.status !== 0) {
1277 if (xmlRetries < 1) {
1278 xmlRetries++;
1279 fetchPages(url, mode, optArg, xmlSession, xmlRetries);
1280 }
1281 else {
1282 var linkId = uniqueIdNum(); // Create a unique ID.
1283 var msg;
1284
1285 if (mode === "hidden") {
1286 msg = "Error retrieving hidden thumbnails";
1287 bbbStatus("hidden", "error");
1288 }
1289 else if (mode === "thumbnails" || mode === "endless") {
1290 msg = "Error retrieving post information";
1291 bbbStatus("posts", "error");
1292 }
1293 else if (mode === "post_comments") {
1294 msg = "Error retrieving comment information";
1295 bbbStatus("post_comments", "error");
1296 }
1297 else if (mode === "paginator")
1298 msg = "Error updating paginator";
1299
1300 var noticeMsg = bbbNotice(msg + ' (HTML Code: ' + xmlhttp.status + ' ' + xmlhttp.statusText + '). (<a id="' + linkId + '" href="#">Retry</a>)', -1);
1301
1302 document.getElementById(linkId).addEventListener("click", function(event) {
1303 closeBbbNoticeMsg(noticeMsg);
1304 searchPages(mode, optArg);
1305 event.preventDefault();
1306 }, false);
1307 }
1308 }
1309 }
1310 };
1311 xmlhttp.open("GET", url, true);
1312 xmlhttp.send(null);
1313 }
1314 }
1315
1316 function replacePaginator(el) {
1317 // Replace the contents inside the paginator div so as to preserve the original div and any event listeners attached to it.
1318 var oldPag = getPaginator();
1319 var newPag = getPaginator(el);
1320
1321 if (oldPag && newPag)
1322 oldPag.innerHTML = newPag.innerHTML;
1323 }
1324
1325 function replaceComments(docEl, postId) {
1326 // Fix hidden comments with information from a post.
1327 var divId = "post_" + postId;
1328 var commentDiv = document.getElementById(divId);
1329 var commentSection = docEl.getElementsByClassName("comments-for-post")[0];
1330 var comments = commentSection.getElementsByClassName("comment");
1331 var numComments = comments.length;
1332 var toShow = 6; // Number of comments to display.
1333 var post = scrapePost(docEl);
1334 var previewImg = commentDiv.getElementsByTagName("img")[0];
1335 var target = commentDiv.getElementsByClassName("comments-for-post")[0];
1336 var newContent = document.createDocumentFragment();
1337
1338 // Fix the image.
1339 if (post.preview_file_url) {
1340 if (post.file_url) {
1341 commentDiv.setAttribute("data-md5", post.md5);
1342 commentDiv.setAttribute("data-file-ext", post.file_ext);
1343 commentDiv.setAttribute("data-file-url", post.file_url);
1344 commentDiv.setAttribute("data-large-file-url", post.large_file_url);
1345 }
1346
1347 previewImg.src = post.preview_file_url;
1348 previewImg.alt = /([^\/]+)\.\w+$/.exec(post.preview_file_url)[1];
1349 commentDiv.setAttribute("data-preview-file-url", post.preview_file_url);
1350 }
1351
1352 // Fix the comments.
1353 if (numComments > toShow) {
1354 for (var i = 0, toHide = numComments - toShow; i < toHide; i++)
1355 comments[i].style.display = "none";
1356
1357 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>';
1358 }
1359
1360 // Add it all in and get it ready.
1361 while (commentSection.firstElementChild)
1362 newContent.appendChild(commentSection.firstElementChild);
1363
1364 target.appendChild(newContent);
1365
1366 Danbooru.Comment.initialize_all();
1367 $("#" + divId + " .simple_form .dtext-preview").hide();
1368 $("#" + divId + " .simple_form input[value=Preview]").click(Danbooru.Dtext.click_button);
1369 }
1370
1371 function replaceThumbnails(docEl) {
1372 // Replace the thumbnails and paginator with new ones.
1373 var thumbContainer = getThumbContainer(gLoc);
1374 var before = getThumbSibling(gLoc);
1375
1376 if (!thumbContainer) {
1377 bbbNotice("Thumbnail section could not be located.", -1);
1378 return;
1379 }
1380
1381 // Thumb preparation.
1382 var newThumbs = document.createDocumentFragment();
1383 var newPosts = getPosts(docEl);
1384
1385 while (newPosts[0])
1386 newThumbs.appendChild(newPosts[0]);
1387
1388 // New thumbnail container preparation.
1389 var replacement = thumbContainer.cloneNode(false);
1390 var childIndex = 0;
1391
1392 while (thumbContainer.children[childIndex]) {
1393 var child = thumbContainer.children[childIndex];
1394
1395 if (child.tagName !== "ARTICLE")
1396 replacement.appendChild(child);
1397 else
1398 childIndex++;
1399 }
1400
1401 if (!before)
1402 replacement.appendChild(newThumbs);
1403 else
1404 replacement.insertBefore(newThumbs, before);
1405
1406 // Prepare thumbnails.
1407 prepThumbnails(replacement);
1408
1409 // Replace results with new results.
1410 thumbContainer.parentNode.replaceChild(replacement, thumbContainer);
1411
1412 // Replace paginator with new paginator.
1413 replacePaginator(docEl);
1414
1415 // Update the URL with the limit value.
1416 if (allowUserLimit())
1417 history.replaceState((history.state || {}), "", updateURLQuery(location.search, {limit: thumbnail_count}));
1418 }
1419
1420 function replaceHidden(docEl) {
1421 // Fix the hidden image placeholders with information from a post.
1422 var hiddenImgs = document.getElementsByClassName("bbb-hidden-thumb");
1423 var article = hiddenImgs[0];
1424
1425 // Hidden thumbnails no longer exist in the page so stop.
1426 if (!article)
1427 return;
1428
1429 var previewImg = article.getElementsByTagName("img")[0];
1430 var hiddenId = article.getAttribute("data-id");
1431 var bcc = bbb.cache.current;
1432 var post = scrapePost(docEl);
1433
1434 if (String(post.id) !== hiddenId) // Out of sync. Reset.
1435 searchPages("hidden", hiddenId);
1436 else if (post.preview_file_url) { // Update the thumbnail with the correct information.
1437 if (post.file_url) {
1438 article.setAttribute("data-md5", post.md5);
1439 article.setAttribute("data-file-ext", post.file_ext);
1440 article.setAttribute("data-file-url", post.file_url);
1441 article.setAttribute("data-large-file-url", post.large_file_url);
1442
1443 // Fix ddl.
1444 postDDL(article);
1445 }
1446
1447 previewImg.src = post.preview_file_url;
1448 article.setAttribute("data-preview-file-url", post.preview_file_url);
1449
1450 bcc.history.push(hiddenId);
1451 bcc.names[hiddenId] = /[^\/]+$/.exec(post.file_url || post.preview_file_url)[0];
1452
1453 article.bbbRemoveClass("bbb-hidden-thumb");
1454
1455 // Continue to the next image or finish by updating the cache.
1456 if (hiddenImgs[0]) {
1457 hiddenId = hiddenImgs[0].getAttribute("data-id");
1458 searchPages("hidden", hiddenId);
1459 }
1460 else
1461 updateThumbCache();
1462 }
1463 else { // The image information couldn't be found.
1464 bbb.flags.hidden_xml = true; // Flag the XML as active to signal a problem and disable further attempts.
1465
1466 updateThumbCache();
1467 bbbNotice("Error retrieving thumbnail information.", -1);
1468 bbbStatus("hidden", "error");
1469 }
1470 }
1471
1472 function endlessXMLPageHandler(docEl) {
1473 // Take thumbnails from a page and pass them to the queue or retrieve hidden posts as necessary.
1474 bbb.endless.new_paginator = getPaginator(docEl);
1475
1476 if (potentialHiddenPosts(gLoc, docEl) && useAPI())
1477 searchJSON("endless");
1478 else {
1479 var posts = getPosts(docEl);
1480 var newPage = document.createElement("div");
1481 newPage.className = "bbb-endless-page";
1482
1483 while (posts[0])
1484 newPage.appendChild(posts[0]);
1485
1486 endlessQueuePage(newPage);
1487 }
1488 }
1489
1490 function isThere(url) {
1491 // Checks if file exists. Thanks to some random forum!
1492 var req = new XMLHttpRequest(); // XMLHttpRequest object.
1493 try {
1494 req.open("HEAD", url, false);
1495 req.send(null);
1496 return (req.status === 200 ? true : false);
1497 } catch(er) {
1498 return false;
1499 }
1500 }
1501
1502 /* Functions for retrieving page info */
1503 function scrapePost(pageEl) {
1504 // Retrieve info from the current document or a supplied element containing the HTML with it.
1505 var target = pageEl || document;
1506 var postContent = getPostContent(target);
1507 var imgContainer = postContent.container;
1508
1509 if (!imgContainer)
1510 return {};
1511
1512 var postEl = postContent.el;
1513 var postTag = (postEl ? postEl.tagName : undefined);
1514 var dataInfo = [imgContainer.getAttribute("data-file-url"), imgContainer.getAttribute("data-md5"), imgContainer.getAttribute("data-file-ext")];
1515 var directLink = getId("image-resize-link", target) || document.evaluate('.//section[@id="post-information"]/ul/li/a[starts-with(@href, "/data/")]', target, null, 9, null).singleNodeValue;
1516 var twitterInfo = getMeta("twitter:image", target);
1517 var previewInfo = getMeta("og:image", target);
1518 var imgHeight = Number(imgContainer.getAttribute("data-height"));
1519 var imgWidth = Number(imgContainer.getAttribute("data-width"));
1520 var md5 = "";
1521 var ext = "";
1522 var infoValues;
1523 var imgInfo = {
1524 md5: "",
1525 file_ext: "",
1526 file_url: "",
1527 large_file_url: "",
1528 preview_file_url: "",
1529 has_large: undefined,
1530 has_sound: (imgContainer.getAttribute("data-has-sound") === "true" ? true : false),
1531 id: Number(imgContainer.getAttribute("data-id")),
1532 fav_count: Number(imgContainer.getAttribute("data-fav-count")),
1533 has_children: (imgContainer.getAttribute("data-has-children") === "true" ? true : false),
1534 has_active_children: (postTag === "IMG" || postTag === "CANVAS" ? postEl.getAttribute("data-has-active-children") === "true" : !!target.getElementsByClassName("notice-parent")[0]),
1535 parent_id: (imgContainer.getAttribute("data-parent-id") ? Number(imgContainer.getAttribute("data-parent-id")) : null),
1536 rating: imgContainer.getAttribute("data-rating"),
1537 score: Number(imgContainer.getAttribute("data-score")),
1538 tag_string: imgContainer.getAttribute("data-tags"),
1539 pool_string: imgContainer.getAttribute("data-pools"),
1540 uploader_name: imgContainer.getAttribute("data-uploader"),
1541 is_deleted: (getMeta("post-is-deleted", target) === "false" ? false : true),
1542 is_flagged: (getMeta("post-is-flagged", target) === "false" ? false : true),
1543 is_pending: (getId("pending-approval-notice", target) ? true : false),
1544 is_banned: (imgContainer.getAttribute("data-flags").indexOf("banned") < 0 ? false : true),
1545 image_height: imgHeight || null,
1546 image_width: imgWidth || null,
1547 is_hidden: !postEl
1548 };
1549
1550 // Try to extract the file's name and extension.
1551 if (dataInfo[1])
1552 infoValues = dataInfo;
1553 else if (directLink)
1554 infoValues = /data\/(\w+)\.(\w+)/.exec(directLink.href);
1555 else if (twitterInfo)
1556 infoValues = (twitterInfo.indexOf("sample") > -1 ? /data\/sample\/sample-(\w+)\.\w/.exec(twitterInfo) : /data\/(\w+)\.(\w+)/.exec(twitterInfo));
1557 else if (previewInfo)
1558 infoValues = /data\/preview\/(\w+?)\.\w/.exec(previewInfo);
1559
1560 if (infoValues) {
1561 md5 = infoValues[1];
1562 ext = infoValues[2];
1563
1564 // Test for the original image file extension if it is unknown.
1565 if (!ext && imgWidth) {
1566 var testExt = ["jpg", "png", "gif", "jpeg", "webm", "mp4"];
1567
1568 for (var i = 0, il = testExt.length; i < il; i++) {
1569 if (isThere("/data/" + md5 + "." + testExt[i])) {
1570 ext = testExt[i];
1571 break;
1572 }
1573 }
1574 }
1575
1576 var isUgoira = (postTag === "CANVAS" || (ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(imgInfo.tag_string)));
1577 var isAnimatedImg = /(?:^|\s)animated_(?:gif|png)(?:$|\s)/.test(imgInfo.tag_string);
1578
1579 if (isUgoira) {
1580 if (postTag === "CANVAS") {
1581 imgInfo.pixiv_ugoira_frame_data = {
1582 id: undefined, // Don't have this value.
1583 post_id: imgInfo.id,
1584 data: JSON.parse(postEl.getAttribute("data-ugoira-frames")),
1585 content_type: postEl.getAttribute("data-ugoira-content-type").replace(/"/gi, "")
1586 };
1587 }
1588 else {
1589 imgInfo.pixiv_ugoira_frame_data = {
1590 id: "", // Don't have this value.
1591 post_id: imgInfo.id,
1592 data: "",
1593 content_type: ""
1594 };
1595 }
1596 }
1597
1598 imgInfo.has_large = (!isAnimatedImg && ((imgWidth > 850 && ext !== "swf" && ext !== "webm" && ext !== "mp4") || isUgoira) ? true : false);
1599 imgInfo.md5 = md5;
1600 imgInfo.file_ext = ext;
1601 imgInfo.file_url = "/data/" + md5 + "." + ext;
1602 imgInfo.preview_file_url = (!imgHeight || ext === "swf" ? "/images/download-preview.png" : "/data/preview/" + md5 + ".jpg");
1603
1604 if (isUgoira)
1605 imgInfo.large_file_url = "/data/sample/sample-" + md5 + ".webm";
1606 else if (imgInfo.has_large)
1607 imgInfo.large_file_url = "/data/sample/sample-" + md5 + ".jpg";
1608 else
1609 imgInfo.large_file_url = "/data/" + md5 + "." + ext;
1610 }
1611 else if (previewInfo === "/images/download-preview.png")
1612 imgInfo.preview_file_url = "/images/download-preview.png";
1613
1614 return imgInfo;
1615 }
1616
1617 function getId(elId, target) {
1618 // Retrieve an element by ID from either the current document or an element containing it.
1619 if (!target || target === document)
1620 return document.getElementById(elId);
1621 else if (target.id === elId)
1622 return target;
1623 else
1624 return target.querySelector("#" + elId);
1625
1626 return null;
1627 }
1628
1629 function getPostContent(pageEl) {
1630 // Retrieve the post content related elements.
1631 var target = pageEl || document;
1632 var imgContainer = getId("image-container", target);
1633
1634 if (!imgContainer)
1635 return {};
1636
1637 var img = getId("image", target);
1638 var swfObj = imgContainer.getElementsByTagName("object")[0];
1639 var swfEmb = (swfObj ? swfObj.getElementsByTagName("embed")[0] : undefined);
1640 var video = imgContainer.getElementsByTagName("video")[0];
1641 var ugoira = imgContainer.getElementsByTagName("canvas")[0];
1642 var other = document.evaluate('.//a[starts-with(@href, "/data/")]', imgContainer, null, 9, null).singleNodeValue;
1643 var el = swfEmb || video || ugoira || img || other;
1644 var secondaryEl = swfObj; // Other elements related to the main element. Only applies to flash for now.
1645
1646 return {container: imgContainer, el: el, secEl: secondaryEl};
1647 }
1648
1649 function getPosts(target) {
1650 // Return a list of posts depending from the document or a specific element.
1651 if (!target || target === document) // All posts in the document.
1652 return document.getElementsByClassName("post-preview");
1653 else if (!target.bbbHasClass("post-preview")) // All posts in a specific element.
1654 return target.getElementsByClassName("post-preview");
1655 else // Single specific post.
1656 return [target];
1657 }
1658
1659 function getPaginator(target) {
1660 // Return the paginator of the document or a specific element.
1661 if (!target || target === document) // Paginator in the document.
1662 return document.getElementsByClassName("paginator")[0];
1663 else if (!target.bbbHasClass("paginator")) // Paginator in a specific element.
1664 return target.getElementsByClassName("paginator")[0];
1665 else // Single specific paginator.
1666 return target;
1667 }
1668
1669 function getThumbContainer(mode, pageEl) {
1670 // Retrieve the element that contains the thumbnails.
1671 var target = pageEl || document;
1672 var container;
1673
1674 if (mode === "search") {
1675 container = getId("posts", target);
1676 container = (container ? container.getElementsByTagName("div")[0] : undefined);
1677 }
1678 else if (mode === "popular" || mode === "notes" || mode === "popular_view")
1679 container = getId("a-index", target);
1680 else if (mode === "pool" || mode === "favorite_group") {
1681 container = getId("a-show", target);
1682 container = (container ? container.getElementsByTagName("section")[0] : undefined);
1683 }
1684 else if (mode === "favorites")
1685 container = getId("posts", target);
1686
1687 // Can't always depend on the first post so it's used as a fallback.
1688 if (!container) {
1689 var posts = getPosts(target);
1690 var firstPost = posts[0];
1691
1692 if (firstPost)
1693 container = firstPost.parentNode;
1694 }
1695
1696 return container;
1697 }
1698
1699 function getThumbSibling(mode, pageEl) {
1700 // If it exists, retrieve the element that thumbnails should be added before.
1701 var target = pageEl || document;
1702 var sibling;
1703
1704 var posts = getPosts(target);
1705 var numPosts = posts.length;
1706 var lastPost = (numPosts ? posts[numPosts - 1] : undefined);
1707 var lastPostParent = (lastPost ? lastPost.parentNode : undefined);
1708 var thumbContainer = getThumbContainer(mode, target);
1709 var lastPostEl = (lastPostParent && lastPostParent !== thumbContainer && lastPostParent.parentNode === thumbContainer ? lastPostParent : lastPost);
1710
1711 if (lastPostEl) {
1712 var contChildren = thumbContainer.children;
1713
1714 for (var i = contChildren.length - 1; i >= 0; i--) {
1715 if (contChildren[i] === lastPostEl) {
1716 sibling = contChildren[i + 1];
1717 break;
1718 }
1719 }
1720 }
1721 else if (mode === "pool" || mode === "notes" || mode === "favorites" || mode === "favorite_group") {
1722 var paginator = getPaginator(target);
1723 var endlessDiv = getId("bbb-endless-button-div", target);
1724
1725 sibling = endlessDiv || paginator;
1726 }
1727
1728 return sibling;
1729 }
1730
1731 function getPaginatorNextURL(target) {
1732 // Retrieve the next page's URL from the paginator.
1733 var paginator = getPaginator(target);
1734
1735 if (paginator) {
1736 var paginatorLinks = paginator.getElementsByTagName("a");
1737
1738 for (var i = paginatorLinks.length - 1; i >= 0; i--) {
1739 var paginatorLink = paginatorLinks[i];
1740
1741 if (paginatorLink.rel.toLowerCase() === "next" && paginatorLink.href) {
1742 return paginatorLink.href;
1743 }
1744 }
1745 }
1746
1747 return undefined;
1748 }
1749
1750 function getMeta(meta, pageEl) {
1751 // Get a value from an HTML meta tag.
1752 var target = pageEl || document;
1753 var metaTags = target.getElementsByTagName("meta");
1754
1755 for (var i = 0, il = metaTags.length; i < il; i++) {
1756 var tag = metaTags[i];
1757
1758 if (tag.name === meta || tag.getAttribute("property") === meta) {
1759 if (tag.hasAttribute("content"))
1760 return tag.content;
1761 else
1762 return undefined;
1763 }
1764 }
1765
1766 return undefined;
1767 }
1768
1769 function getVar(urlVar, targetUrl) {
1770 // Retrieve a value from a specified/current URL's query string.
1771 // 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=&).
1772 var url = targetUrl;
1773
1774 if (!url)
1775 url = location.search;
1776
1777 var result = url.split(new RegExp("[&\?]" + urlVar))[1];
1778
1779 if (result === undefined)
1780 return undefined;
1781
1782 result = result.split(/[#&]/, 1)[0].split("=", 2)[1];
1783
1784 if (result === undefined)
1785 return null;
1786 else
1787 return result;
1788 }
1789
1790 function getTagVar(urlVar, url) {
1791 // Retrieve a metatag's value from the tag portion of a specified/current URL's query string.
1792 if (!url)
1793 url = location.search;
1794
1795 var tags = getVar("tags", url);
1796 var tag;
1797 var result;
1798
1799 // If the tags parameter isn't provided or has no value, the metatag is undefined.
1800 if (tags === null || tags === undefined)
1801 return undefined;
1802
1803 tags = tags.split(/\+|%20/g);
1804
1805 for (var i = 0, il = tags.length; i < il; i++) {
1806 tag = decodeURIComponent(tags[i]);
1807
1808 if (tag.indexOf(urlVar + ":") === 0)
1809 result = encodeURIComponent(tag.split(":")[1]); // Let the calling function decide whether it wants the decoded tag or not.
1810 }
1811
1812 return result;
1813 }
1814
1815 /* Functions for the settings panel */
1816 function injectSettings() {
1817 var menu = document.getElementById("top");
1818 menu = (menu ? menu.getElementsByTagName("menu")[0] : undefined);
1819
1820 if (!menu)
1821 return;
1822
1823 var menuItems = menu.getElementsByTagName("li");
1824 var numMenuItems = menu.getElementsByTagName("li").length;
1825 var moreItem = menuItems[numMenuItems - 1];
1826
1827 for (var i = numMenuItems - 1; i >= 0; i--) {
1828 var menuLink = menuItems[i];
1829
1830 if (menuLink.textContent.indexOf("More") > -1) {
1831 moreItem = menuLink;
1832 break;
1833 }
1834 }
1835
1836 var link = document.createElement("a");
1837 link.href = "#";
1838 link.innerHTML = "BBB Settings";
1839 link.addEventListener("click", function(event) {
1840 openMenu();
1841
1842 event.preventDefault();
1843 }, false);
1844
1845 var item = document.createElement("li");
1846 item.appendChild(link);
1847
1848 if (moreItem)
1849 menu.insertBefore(item, moreItem);
1850 else
1851 menu.appendChild(item);
1852
1853 window.addEventListener("resize", adjustMenuTimer, false);
1854 }
1855
1856 function openMenu() {
1857 if (bbb.el.menu.window)
1858 return;
1859
1860 loadSettings();
1861 createMenu();
1862 }
1863
1864 function reloadMenu() {
1865 removeMenu();
1866 createMenu();
1867 }
1868
1869 function createMenu() {
1870 var menu = bbb.el.menu.window = document.createElement("div");
1871 menu.id = "bbb-menu";
1872 menu.style.visibility = "hidden";
1873
1874 var tip = bbb.el.menu.tip = document.createElement("div");
1875 tip.id = "bbb-expl";
1876 menu.appendChild(tip);
1877
1878 var header = document.createElement("h1");
1879 header.innerHTML = "Better Better Booru Settings";
1880 header.style.textAlign = "center";
1881 menu.appendChild(header);
1882
1883 var tabBar = document.createElement("div");
1884 tabBar.style.padding = "0px 15px";
1885 tabBar.addEventListener("click", function(event) {
1886 var target = event.target;
1887
1888 if (target.href)
1889 changeTab(target);
1890
1891 event.preventDefault();
1892 }, false);
1893 menu.appendChild(tabBar);
1894
1895 var generalTab = bbb.el.menu.generalTab = document.createElement("a");
1896 generalTab.name = "general";
1897 generalTab.href = "#";
1898 generalTab.innerHTML = "General";
1899 generalTab.className = "bbb-tab bbb-active-tab";
1900 tabBar.appendChild(generalTab);
1901
1902 var blacklistTab = bbb.el.menu.blacklistTab = document.createElement("a");
1903 blacklistTab.name = "blacklist";
1904 blacklistTab.href = "#";
1905 blacklistTab.innerHTML = "Blacklist";
1906 blacklistTab.className = "bbb-tab";
1907 tabBar.appendChild(blacklistTab);
1908
1909 var borderTab = bbb.el.menu.borderTab = document.createElement("a");
1910 borderTab.name = "borders";
1911 borderTab.href = "#";
1912 borderTab.innerHTML = "Borders";
1913 borderTab.className = "bbb-tab";
1914 tabBar.appendChild(borderTab);
1915
1916 var layoutTab = bbb.el.menu.layoutTab = document.createElement("a");
1917 layoutTab.name = "layout";
1918 layoutTab.href = "#";
1919 layoutTab.innerHTML = "Layout";
1920 layoutTab.className = "bbb-tab";
1921 tabBar.appendChild(layoutTab);
1922
1923 var prefTab = bbb.el.menu.prefTab = document.createElement("a");
1924 prefTab.name = "pref";
1925 prefTab.href = "#";
1926 prefTab.innerHTML = "Preferences";
1927 prefTab.className = "bbb-tab";
1928 tabBar.appendChild(prefTab);
1929
1930 var helpTab = bbb.el.menu.helpTab = document.createElement("a");
1931 helpTab.name = "help";
1932 helpTab.href = "#";
1933 helpTab.innerHTML = "Help";
1934 helpTab.className = "bbb-tab";
1935 tabBar.appendChild(helpTab);
1936
1937 var scrollDiv = bbb.el.menu.scrollDiv = document.createElement("div");
1938 scrollDiv.className = "bbb-scroll-div";
1939 menu.appendChild(scrollDiv);
1940 scrollDiv.scrollTop = 0;
1941
1942 var generalPage = bbb.el.menu.generalPage = document.createElement("div");
1943 generalPage.className = "bbb-page";
1944 generalPage.style.display = "block";
1945 scrollDiv.appendChild(generalPage);
1946
1947 generalPage.bbbSection(bbb.sections.browse);
1948 generalPage.bbbSection(bbb.sections.control);
1949 generalPage.bbbSection(bbb.sections.endless);
1950 generalPage.bbbSection(bbb.sections.misc);
1951
1952 var blacklistPage = bbb.el.menu.blacklistPage = document.createElement("div");
1953 blacklistPage.className = "bbb-page";
1954 scrollDiv.appendChild(blacklistPage);
1955
1956 blacklistPage.bbbSection(bbb.sections.blacklist_options);
1957 blacklistPage.bbbBlacklistSection();
1958
1959 var layoutPage = bbb.el.menu.layoutPage = document.createElement("div");
1960 layoutPage.className = "bbb-page";
1961 scrollDiv.appendChild(layoutPage);
1962
1963 layoutPage.bbbSection(bbb.sections.sidebar);
1964 layoutPage.bbbSection(bbb.sections.notices);
1965 layoutPage.bbbSection(bbb.sections.misc_layout);
1966
1967 var bordersPage = bbb.el.menu.bordersPage = document.createElement("div");
1968 bordersPage.className = "bbb-page";
1969 scrollDiv.appendChild(bordersPage);
1970
1971 bordersPage.bbbSection(bbb.sections.border_options);
1972 bordersPage.bbbSection(bbb.sections.status_borders);
1973 bordersPage.bbbSection(bbb.sections.tag_borders);
1974
1975 var prefPage = bbb.el.menu.prefPage = document.createElement("div");
1976 prefPage.className = "bbb-page";
1977 scrollDiv.appendChild(prefPage);
1978
1979 prefPage.bbbSection(bbb.sections.script_settings);
1980 prefPage.bbbBackupSection();
1981
1982 var helpPage = bbb.el.menu.helpPage = document.createElement("div");
1983 helpPage.className = "bbb-page";
1984 scrollDiv.appendChild(helpPage);
1985
1986 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 coupled with percent signs:<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.</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 not in an any active pools.</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>');
1987 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>F</b> - Open quick search.</li><li><b>Shift + F</b> - Reset quick search.</li></ul>');
1988 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>.');
1989 helpPage.bbbTocSection();
1990
1991 var close = document.createElement("a");
1992 close.innerHTML = "Save & Close";
1993 close.href = "#";
1994 close.className = "bbb-button";
1995 close.style.marginRight = "15px";
1996 close.addEventListener("click", function(event) {
1997 removeMenu();
1998 saveSettings();
1999 event.preventDefault();
2000 }, false);
2001
2002 var cancel = document.createElement("a");
2003 cancel.innerHTML = "Cancel";
2004 cancel.href = "#";
2005 cancel.className = "bbb-button";
2006 cancel.addEventListener("click", function(event) {
2007 removeMenu();
2008 loadSettings();
2009 event.preventDefault();
2010 }, false);
2011
2012 var reset = document.createElement("a");
2013 reset.innerHTML = "Reset to Defaults";
2014 reset.href = "#";
2015 reset.className = "bbb-button";
2016 reset.style.cssFloat = "right";
2017 reset.style.color = "#ff1100";
2018 reset.addEventListener("click", function(event) {
2019 loadDefaults();
2020 reloadMenu();
2021 event.preventDefault();
2022 }, false);
2023
2024 menu.appendChild(close);
2025 menu.appendChild(cancel);
2026 menu.appendChild(reset);
2027
2028 // Add menu to the DOM and manipulate the dimensions.
2029 document.body.appendChild(menu);
2030
2031 var viewHeight = document.documentElement.clientHeight;
2032 var barWidth = scrollbarWidth();
2033 var scrollDivDiff = menu.offsetHeight - scrollDiv.clientHeight;
2034
2035 scrollDiv.style.maxHeight = viewHeight - scrollDiv.bbbGetPadding().height - scrollDivDiff - 50 + "px"; // Subtract 50 for margins (25 each).
2036 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.
2037 scrollDiv.style.paddingLeft = barWidth + 3 + "px";
2038
2039 var menuWidth = menu.offsetWidth;
2040
2041 menu.style.marginLeft = -menuWidth / 2 + "px";
2042 menu.style.visibility = "visible";
2043 }
2044
2045 function createSection(section) {
2046 var sectionFrag = document.createDocumentFragment();
2047 var i, il; // Loop variables.
2048
2049 if (section.header) {
2050 var sectionHeader = document.createElement("h2");
2051 sectionHeader.innerHTML = section.header;
2052 sectionHeader.className = "bbb-header";
2053 sectionFrag.appendChild(sectionHeader);
2054 }
2055
2056 if (section.text) {
2057 var sectionText = document.createElement("div");
2058 sectionText.innerHTML = section.text;
2059 sectionText.className = "bbb-section-text";
2060 sectionFrag.appendChild(sectionText);
2061 }
2062
2063 var sectionDiv = document.createElement("div");
2064 sectionDiv.className = "bbb-section-options";
2065 sectionFrag.appendChild(sectionDiv);
2066
2067 if (section.type === "general") {
2068 var settingList = section.settings;
2069 var sll = settingList.length;
2070 var halfway = (sll > 1 ? Math.ceil(sll / 2) : 0);
2071
2072 var leftSide = document.createElement("div");
2073 leftSide.className = "bbb-section-options-left";
2074 sectionDiv.appendChild(leftSide);
2075
2076 var rightSide = document.createElement("div");
2077 rightSide.className = "bbb-section-options-right";
2078 sectionDiv.appendChild(rightSide);
2079
2080 var optionTarget = leftSide;
2081
2082 for (i = 0; i < sll; i++) {
2083 var settingName = settingList[i];
2084
2085 if (halfway && i >= halfway)
2086 optionTarget = rightSide;
2087
2088 var newOption = createOption(settingName);
2089 optionTarget.appendChild(newOption);
2090 }
2091 }
2092 else if (section.type === "border") {
2093 var borderSettings = bbb.user[section.settings];
2094
2095 for (i = 0, il = borderSettings.length; i < il; i++) {
2096 var newBorderOption = createBorderOption(borderSettings, i);
2097 sectionDiv.appendChild(newBorderOption);
2098 }
2099
2100 var indexWrapper = document.createElement("div");
2101 indexWrapper.setAttribute("data-bbb-index", i);
2102 sectionDiv.appendChild(indexWrapper);
2103
2104 var borderDivider = document.createElement("div");
2105 borderDivider.className = "bbb-border-divider";
2106 indexWrapper.appendChild(borderDivider);
2107 }
2108
2109 return sectionFrag;
2110 }
2111
2112 Element.prototype.bbbSection = function(section) {
2113 this.appendChild(createSection(section));
2114 };
2115
2116 function createOption(settingName) {
2117 var optionObject = bbb.options[settingName];
2118 var userSetting = bbb.user[settingName];
2119 var i, il; // Loop variables.
2120
2121 var label = document.createElement("label");
2122 label.className = "bbb-general-label";
2123
2124 var textSpan = document.createElement("span");
2125 textSpan.className = "bbb-general-text";
2126 textSpan.innerHTML = optionObject.label;
2127 label.appendChild(textSpan);
2128
2129 var inputSpan = document.createElement("span");
2130 inputSpan.className = "bbb-general-input";
2131 label.appendChild(inputSpan);
2132
2133 var item;
2134 var itemFrag = document.createDocumentFragment();
2135
2136 switch (optionObject.type) {
2137 case "dropdown":
2138 var txtOptions = optionObject.txtOptions;
2139 var numRange = optionObject.numRange;
2140 var numList = optionObject.numList;
2141 var selectOption;
2142
2143 item = document.createElement("select");
2144 item.name = settingName;
2145
2146 if (txtOptions) {
2147 for (i = 0, il = txtOptions.length; i < il; i++) {
2148 var txtOption = txtOptions[i].split(":");
2149
2150 selectOption = document.createElement("option");
2151 selectOption.innerHTML = txtOption[0];
2152 selectOption.value = txtOption[1];
2153
2154 if (selectOption.value === String(userSetting))
2155 selectOption.selected = true;
2156
2157 item.appendChild(selectOption);
2158 }
2159 }
2160
2161 if (numList) {
2162 for (i = 0, il = numList.length; i < il; i++) {
2163 selectOption = document.createElement("option");
2164 selectOption.innerHTML = numList[i];
2165 selectOption.value = numList[i];
2166
2167 if (selectOption.value === String(userSetting))
2168 selectOption.selected = true;
2169
2170 item.appendChild(selectOption);
2171 }
2172 }
2173
2174 if (numRange) {
2175 var end = numRange[1];
2176
2177 for (i = numRange[0]; i <= end; i++) {
2178 selectOption = document.createElement("option");
2179 selectOption.innerHTML = i;
2180 selectOption.value = i;
2181
2182 if (selectOption.value === String(userSetting))
2183 selectOption.selected = true;
2184
2185 item.appendChild(selectOption);
2186 }
2187 }
2188
2189 item.addEventListener("change", function() {
2190 var selected = this.value;
2191 bbb.user[settingName] = (bbbIsNum(selected) ? Number(selected) : selected);
2192 bbb.settings.changed[settingName] = true;
2193 }, false);
2194 itemFrag.appendChild(item);
2195 break;
2196 case "checkbox":
2197 item = document.createElement("input");
2198 item.name = settingName;
2199 item.type = "checkbox";
2200 item.checked = userSetting;
2201 item.addEventListener("click", function() {
2202 bbb.user[settingName] = this.checked;
2203 bbb.settings.changed[settingName] = true;
2204 }, false);
2205 itemFrag.appendChild(item);
2206 break;
2207 case "text":
2208 item = document.createElement("input");
2209 item.name = settingName;
2210 item.type = "text";
2211 item.value = userSetting;
2212 item.addEventListener("change", function() {
2213 bbb.user[settingName] = (optionObject.isTagInput ? this.value.bbbTagClean() : this.value.bbbSpaceClean());
2214 bbb.settings.changed[settingName] = true;
2215 }, false);
2216 itemFrag.appendChild(item);
2217
2218 if (optionObject.isTagInput) {
2219 var tagExpand = document.createElement("a");
2220 tagExpand.href = "#";
2221 tagExpand.className = "bbb-edit-link";
2222 tagExpand.innerHTML = "»";
2223 tagExpand.addEventListener("click", function(event) {
2224 tagEditWindow(item, bbb.user, settingName);
2225 event.preventDefault();
2226 }, false);
2227 itemFrag.appendChild(tagExpand);
2228 }
2229 break;
2230 case "number":
2231 item = document.createElement("input");
2232 item.name = settingName;
2233 item.type = "text";
2234 item.value = userSetting;
2235 item.addEventListener("change", function() {
2236 bbb.user[settingName] = Number(this.value);
2237 bbb.settings.changed[settingName] = true;
2238 }, false);
2239 itemFrag.appendChild(item);
2240 break;
2241 default:
2242 bbbNotice('Unexpected menu object type for "' + optionObject.label + '". (Type: ' + optionObject.type + ')', -1);
2243 return label;
2244 }
2245 inputSpan.appendChild(itemFrag);
2246
2247 var explLink = document.createElement("a");
2248 explLink.innerHTML = "?";
2249 explLink.href = "#";
2250 explLink.className = "bbb-expl-link";
2251 explLink.bbbSetTip(bbb.options[settingName].expl);
2252 inputSpan.appendChild(explLink);
2253
2254 return label;
2255 }
2256
2257 function createBorderOption(borderSettings, index) {
2258 var borderItem = borderSettings[index];
2259 var isStatus = (borderItem.class_name ? true : false);
2260
2261 var borderSpacer = document.createElement("span");
2262 borderSpacer.className = "bbb-border-spacer";
2263
2264 var indexWrapper = document.createElement("div");
2265 indexWrapper.setAttribute("data-bbb-index", index);
2266
2267 var borderDivider = document.createElement("div");
2268 borderDivider.className = "bbb-border-divider";
2269 indexWrapper.appendChild(borderDivider);
2270
2271 var borderDiv = document.createElement("div");
2272 borderDiv.className = "bbb-border-div";
2273 indexWrapper.appendChild(borderDiv);
2274
2275 var borderBarDiv = document.createElement("div");
2276 borderBarDiv.className = "bbb-border-bar";
2277 borderDiv.appendChild(borderBarDiv);
2278
2279 var enableLabel = document.createElement("label");
2280 enableLabel.innerHTML = "Enabled:";
2281 borderBarDiv.appendChild(enableLabel);
2282
2283 var enableBox = document.createElement("input");
2284 enableBox.type = "checkbox";
2285 enableBox.checked = borderItem.is_enabled;
2286 enableBox.addEventListener("click", function() { borderItem.is_enabled = this.checked; }, false);
2287 enableLabel.appendChild(enableBox);
2288
2289 var editSpan = document.createElement("span");
2290 editSpan.style.cssFloat = "right";
2291 borderBarDiv.appendChild(editSpan);
2292
2293 var moveButton = document.createElement("a");
2294 moveButton.href = "#";
2295 moveButton.innerHTML = "Move";
2296 moveButton.className = "bbb-border-button";
2297 moveButton.addEventListener("click", function(event) {
2298 moveBorder(borderSettings, indexWrapper);
2299 event.preventDefault();
2300 }, false);
2301 moveButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to move this border.");
2302 editSpan.appendChild(moveButton);
2303
2304 var previewButton = document.createElement("a");
2305 previewButton.href = "#";
2306 previewButton.innerHTML = "Preview";
2307 previewButton.className = "bbb-border-button";
2308 previewButton.addEventListener("click", function(event) { event.preventDefault(); }, false);
2309 previewButton.bbbBorderPreview(borderItem);
2310 editSpan.appendChild(previewButton);
2311
2312 if (!isStatus) {
2313 var deleteButton = document.createElement("a");
2314 deleteButton.href = "#";
2315 deleteButton.innerHTML = "Delete";
2316 deleteButton.className = "bbb-border-button";
2317 deleteButton.addEventListener("click", function(event) {
2318 deleteBorder(borderSettings, indexWrapper);
2319 event.preventDefault();
2320 }, false);
2321 editSpan.appendChild(deleteButton);
2322
2323 var newButton = document.createElement("a");
2324 newButton.href = "#";
2325 newButton.innerHTML = "New";
2326 newButton.className = "bbb-border-button";
2327 newButton.addEventListener("click", function(event) {
2328 createBorder(borderSettings, indexWrapper);
2329 event.preventDefault();
2330 }, false);
2331 newButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to create a border.");
2332 editSpan.appendChild(newButton);
2333 }
2334
2335 editSpan.appendChild(borderSpacer.cloneNode(false));
2336
2337 var helpButton = document.createElement("a");
2338 helpButton.href = "#";
2339 helpButton.innerHTML = "Help";
2340 helpButton.className = "bbb-border-button";
2341 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.");
2342 editSpan.appendChild(helpButton);
2343
2344 var borderSettingsDiv = document.createElement("div");
2345 borderSettingsDiv.className = "bbb-border-settings";
2346 borderDiv.appendChild(borderSettingsDiv);
2347
2348 var nameLabel = document.createElement("label");
2349 nameLabel.className = "bbb-border-name";
2350 borderSettingsDiv.appendChild(nameLabel);
2351
2352 if (isStatus)
2353 nameLabel.innerHTML = "Status:" + borderItem.tags;
2354 else {
2355 nameLabel.innerHTML = "Tags:";
2356
2357 var nameInput = document.createElement("input");
2358 nameInput.type = "text";
2359 nameInput.value = borderItem.tags;
2360 nameInput.addEventListener("change", function() { borderItem.tags = this.value.bbbTagClean(); }, false);
2361 nameLabel.appendChild(nameInput);
2362
2363 var nameExpand = document.createElement("a");
2364 nameExpand.href = "#";
2365 nameExpand.className = "bbb-edit-link";
2366 nameExpand.innerHTML = "»";
2367 nameExpand.addEventListener("click", function(event) {
2368 tagEditWindow(nameInput, borderItem, "tags");
2369 event.preventDefault();
2370 }, false);
2371 nameLabel.appendChild(nameExpand);
2372 }
2373
2374 var colorLabel = document.createElement("label");
2375 colorLabel.innerHTML = "Color:";
2376 colorLabel.className = "bbb-border-color";
2377 borderSettingsDiv.appendChild(colorLabel);
2378
2379 var colorInput = document.createElement("input");
2380 colorInput.type = "text";
2381 colorInput.value = borderItem.border_color;
2382 colorInput.addEventListener("change", function() { borderItem.border_color = this.value.bbbSpaceClean(); }, false);
2383 colorLabel.appendChild(colorInput);
2384
2385 var styleLabel = document.createElement("label");
2386 styleLabel.innerHTML = "Style:";
2387 styleLabel.className = "bbb-border-style";
2388 borderSettingsDiv.appendChild(styleLabel);
2389
2390 var styleDrop = document.createElement("select");
2391 styleDrop.addEventListener("change", function() { borderItem.border_style = this.value; }, false);
2392 styleLabel.appendChild(styleDrop);
2393
2394 var solidOption = document.createElement("option");
2395 solidOption.innerHTML = "solid";
2396 solidOption.value = "solid";
2397 styleDrop.appendChild(solidOption);
2398
2399 var dashedOption = document.createElement("option");
2400 dashedOption.innerHTML = "dashed";
2401 dashedOption.value = "dashed";
2402 styleDrop.appendChild(dashedOption);
2403
2404 var dottedOption = document.createElement("option");
2405 dottedOption.innerHTML = "dotted";
2406 dottedOption.value = "dotted";
2407 styleDrop.appendChild(dottedOption);
2408
2409 var doubleOption = document.createElement("option");
2410 doubleOption.innerHTML = "double";
2411 doubleOption.value = "double";
2412 styleDrop.appendChild(doubleOption);
2413
2414 var styleOptions = styleDrop.getElementsByTagName("option");
2415
2416 for (var i = 0; i < 4; i++) {
2417 if (styleOptions[i].value === borderItem.border_style) {
2418 styleOptions[i].selected = true;
2419 break;
2420 }
2421 }
2422
2423 return indexWrapper;
2424 }
2425
2426 function createTextSection(header, text) {
2427 var sectionFrag = document.createDocumentFragment();
2428
2429 if (header) {
2430 var sectionHeader = document.createElement("h2");
2431 sectionHeader.innerHTML = header;
2432 sectionHeader.className = "bbb-header";
2433 sectionFrag.appendChild(sectionHeader);
2434 }
2435
2436 if (text) {
2437 var desc = document.createElement("div");
2438 desc.innerHTML = text;
2439 desc.className = "bbb-section-text";
2440 sectionFrag.appendChild(desc);
2441 }
2442
2443 return sectionFrag;
2444 }
2445
2446 Element.prototype.bbbTextSection = function(header, text) {
2447 this.appendChild(createTextSection(header, text));
2448 };
2449
2450 function createBackupSection() {
2451 var sectionFrag = document.createDocumentFragment();
2452
2453 var sectionHeader = document.createElement("h2");
2454 sectionHeader.innerHTML = "Backup/Restore Settings";
2455 sectionHeader.className = "bbb-header";
2456 sectionFrag.appendChild(sectionHeader);
2457
2458 var sectionDiv = document.createElement("div");
2459 sectionDiv.className = "bbb-section-options";
2460 sectionFrag.appendChild(sectionDiv);
2461
2462 var backupTextarea = bbb.el.menu.backupTextarea = document.createElement("textarea");
2463 backupTextarea.className = "bbb-backup-area";
2464 sectionDiv.appendChild(backupTextarea);
2465
2466 var buttonDiv = document.createElement("div");
2467 buttonDiv.className = "bbb-section-options";
2468 sectionFrag.appendChild(buttonDiv);
2469
2470 var textBackup = document.createElement("a");
2471 textBackup.innerHTML = "Create Backup Text";
2472 textBackup.href = "#";
2473 textBackup.className = "bbb-button";
2474 textBackup.style.marginRight = "15px";
2475 textBackup.addEventListener("click", function(event) {
2476 createBackupText();
2477 event.preventDefault();
2478 }, false);
2479 buttonDiv.appendChild(textBackup);
2480
2481 var pageBackup = document.createElement("a");
2482 pageBackup.innerHTML = "Create Backup Page";
2483 pageBackup.href = "#";
2484 pageBackup.className = "bbb-button";
2485 pageBackup.style.marginRight = "15px";
2486 pageBackup.addEventListener("click", function(event) {
2487 createBackupPage();
2488 event.preventDefault();
2489 }, false);
2490 buttonDiv.appendChild(pageBackup);
2491
2492 var rightButtons = document.createElement("span");
2493 rightButtons.style.cssFloat = "right";
2494 buttonDiv.appendChild(rightButtons);
2495
2496 var restoreBackup = document.createElement("a");
2497 restoreBackup.innerHTML = "Restore Backup";
2498 restoreBackup.style.marginRight = "15px";
2499 restoreBackup.href = "#";
2500 restoreBackup.className = "bbb-button";
2501 restoreBackup.addEventListener("click", function(event) {
2502 restoreBackupText();
2503 event.preventDefault();
2504 }, false);
2505 rightButtons.appendChild(restoreBackup);
2506
2507 var helpButton = document.createElement("a");
2508 helpButton.innerHTML = "Help";
2509 helpButton.href = "#";
2510 helpButton.className = "bbb-button";
2511 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 a text backup will provide a plain text format backup in the area provided that can be copied and saved where desired. Creating a backup page will open a new page that can be saved with the browser's \"save page\" or bookmark options. <br><br>To restore a backup, copy and paste the desired backup into the provided area and click \"restore backup\".");
2512 rightButtons.appendChild(helpButton);
2513
2514 return sectionFrag;
2515 }
2516
2517 Element.prototype.bbbBackupSection = function() {
2518 this.appendChild(createBackupSection());
2519 };
2520
2521 function createBlacklistSection() {
2522 var sectionFrag = document.createDocumentFragment();
2523
2524 var sectionHeader = document.createElement("h2");
2525 sectionHeader.innerHTML = "Blacklist";
2526 sectionHeader.className = "bbb-header";
2527 sectionFrag.appendChild(sectionHeader);
2528
2529 var sectionDiv = document.createElement("div");
2530 sectionDiv.className = "bbb-section-options";
2531 sectionFrag.appendChild(sectionDiv);
2532
2533 var blacklistTextarea = bbb.el.menu.blacklistTextarea = document.createElement("textarea");
2534 blacklistTextarea.className = "bbb-blacklist-area";
2535 blacklistTextarea.value = searchSingleToMulti(bbb.user.script_blacklisted_tags);
2536 blacklistTextarea.addEventListener("change", function() { bbb.user.script_blacklisted_tags = searchMultiToSingle(blacklistTextarea.value); }, false);
2537 sectionDiv.appendChild(blacklistTextarea);
2538
2539 var buttonDiv = document.createElement("div");
2540 buttonDiv.className = "bbb-section-options";
2541 sectionFrag.appendChild(buttonDiv);
2542
2543 var formatButton = document.createElement("a");
2544 formatButton.innerHTML = "Format";
2545 formatButton.href = "#";
2546 formatButton.className = "bbb-button";
2547 formatButton.addEventListener("click", function(event) {
2548 var textareaString = searchMultiToSingle(blacklistTextarea.value);
2549
2550 blacklistTextarea.value = searchSingleToMulti(textareaString);
2551 event.preventDefault();
2552 }, false);
2553 buttonDiv.appendChild(formatButton);
2554
2555 var helpButton = document.createElement("a");
2556 helpButton.innerHTML = "Help";
2557 helpButton.href = "#";
2558 helpButton.className = "bbb-button";
2559 helpButton.style.cssFloat = "right";
2560 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 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.");
2561 buttonDiv.appendChild(helpButton);
2562
2563 return sectionFrag;
2564 }
2565
2566 Element.prototype.bbbBlacklistSection = function() {
2567 this.appendChild(createBlacklistSection());
2568 };
2569
2570 function createTocSection(page) {
2571 // Generate a Table of Contents based on the page's current section headers.
2572 var sectionFrag = document.createDocumentFragment();
2573 var pageSections = page.getElementsByTagName("h2");
2574
2575 var sectionHeader = document.createElement("h2");
2576 sectionHeader.innerHTML = "Table of Contents";
2577 sectionHeader.className = "bbb-header";
2578 sectionFrag.appendChild(sectionHeader);
2579
2580 var sectionText = document.createElement("div");
2581 sectionText.className = "bbb-section-text";
2582 sectionFrag.appendChild(sectionText);
2583
2584 var tocList = document.createElement("ol");
2585 tocList.className = "bbb-toc";
2586 sectionText.appendChild(tocList);
2587
2588 for (var i = 0, il = pageSections.length; i < il;) {
2589 var listItem = document.createElement("li");
2590 tocList.appendChild(listItem);
2591
2592 var linkItem = document.createElement("a");
2593 linkItem.textContent = pageSections[i].textContent;
2594 linkItem.href = "#" + (++i);
2595 listItem.appendChild(linkItem);
2596 }
2597
2598 tocList.addEventListener("click", function (event) {
2599 var target = event.target;
2600 var targetValue = target.href;
2601
2602 if (targetValue) {
2603 var sectionTop = pageSections[targetValue.split("#")[1]].offsetTop;
2604 bbb.el.menu.scrollDiv.scrollTop = sectionTop;
2605 event.preventDefault();
2606 }
2607 }, false);
2608
2609 return sectionFrag;
2610 }
2611
2612 Element.prototype.bbbTocSection = function() {
2613 var page = this;
2614 page.insertBefore(createTocSection(page), page.firstElementChild);
2615 };
2616
2617 function newOption(type, def, lbl, expl, optPropObject) {
2618 /*
2619 * Option type notes
2620 * =================
2621 * By specifying a unique type, you can create a specialized menu option.
2622 *
2623 * Checkbox, text, and number do not require any extra properties.
2624 *
2625 * Dropdown requires either txtOptions, numRange, or numList.
2626 * txtOptions = Array containing a list of options and their values separated by a colon. (ex: ["option1:value1", "option2:value2"])
2627 * numRange = Array containing the starting and ending numbers of the number range.
2628 * numList = Array containing a list of the desired numbers.
2629 * If more than one of these is provided, they are added to the list in this order: txtOptions, numList, numRange
2630 */
2631
2632 var option = {
2633 type: type,
2634 def: def, // Default.
2635 label: lbl,
2636 expl: expl // Explanation.
2637 };
2638
2639 if (optPropObject) { // Additional properties provided in the form of an object.
2640 for (var i in optPropObject) {
2641 if (optPropObject.hasOwnProperty(i))
2642 option[i] = optPropObject[i];
2643 }
2644 }
2645
2646 return option;
2647 }
2648
2649 function newSection(type, settingList, header, text) {
2650 /*
2651 * Section type notes
2652 * ==================
2653 * Current section types are general and border.
2654 *
2655 * The setting list for general sections are provided in the form of an array containing the setting names as strings.
2656 * The setting list for border sections is the setting name containing the borders as a string.
2657 */
2658 return {
2659 type: type,
2660 settings: settingList,
2661 header: header,
2662 text: text
2663 };
2664 }
2665
2666 function newBorder(tags, isEnabled, color, style, className) {
2667 return {
2668 tags: tags,
2669 is_enabled: isEnabled,
2670 border_color: color,
2671 border_style: style,
2672 class_name: className
2673 };
2674 }
2675
2676 function borderSet() {
2677 var formatted = [];
2678
2679 for (var i = 0, il = arguments.length; i < il; i++) {
2680 var border = arguments[i];
2681
2682 formatted.push(newBorder(border[0], border[1], border[2], border[3], border[4]));
2683 }
2684
2685 return formatted;
2686 }
2687
2688 function resetBorderElements(section) {
2689 // Reset the list of border items after moving or creating a new border.
2690 var borderElements = section.children;
2691
2692 for (var i = 0, il = borderElements.length; i < il; i++) {
2693 var borderElement = borderElements[i];
2694
2695 borderElement.bbbRemoveClass("bbb-no-highlight");
2696 borderElement.setAttribute("data-bbb-index", i);
2697 }
2698 }
2699
2700 function deleteBorder(borderSettings, borderElement) {
2701 // Remove a border and if it's the last border, create a blank disabled one.
2702 var section = borderElement.parentNode;
2703 var index = Number(borderElement.getAttribute("data-bbb-index"));
2704
2705 section.removeChild(borderElement);
2706 borderSettings.splice(index,1);
2707
2708 if (!borderSettings[0]) {
2709 // If no borders are left, add a new blank border.
2710 var newBorderItem = newBorder("", false, "#000000", "solid");
2711 borderSettings.push(newBorderItem);
2712
2713 var newBorderElement = createBorderOption(borderSettings, 0);
2714 section.insertBefore(newBorderElement, section.firstElementChild);
2715 }
2716
2717 resetBorderElements(section);
2718 }
2719
2720 function moveBorder(borderSettings, borderElement) {
2721 // Prepare to move a border and wait for the user to click where it'll go.
2722 var section = borderElement.parentNode;
2723 var index = Number(borderElement.getAttribute("data-bbb-index"));
2724
2725 borderElement.bbbAddClass("bbb-no-highlight");
2726 borderElement.nextSibling.bbbAddClass("bbb-no-highlight");
2727 bbb.borderEdit = {mode: "move", settings: borderSettings, section: section, index: index, el: borderElement};
2728 section.bbbAddClass("bbb-insert-highlight");
2729 bbb.el.menu.window.addEventListener("click", insertBorder, true);
2730 }
2731
2732 function createBorder(borderSettings, borderElement) {
2733 // Prepare to create a border and wait for the user to click where it'll go.
2734 var section = borderElement.parentNode;
2735
2736 bbb.borderEdit = {mode: "new", settings: borderSettings, section: section};
2737 section.bbbAddClass("bbb-insert-highlight");
2738 bbb.el.menu.window.addEventListener("click", insertBorder, true);
2739 }
2740
2741 function insertBorder(event) {
2742 // Place either a new or moved border where indicated.
2743 var target = event.target;
2744 var section = bbb.borderEdit.section;
2745
2746 if (target.className === "bbb-border-divider") {
2747 var newIndex = Number(target.parentNode.getAttribute("data-bbb-index"));
2748 var borderSettings = bbb.borderEdit.settings;
2749
2750 if (bbb.borderEdit.mode === "new") { // Make a new border.
2751 var newBorderItem = newBorder("", false, "#000000", "solid");
2752 borderSettings.splice(newIndex, 0, newBorderItem);
2753
2754 var newBorderElement = createBorderOption(borderSettings, newIndex);
2755
2756 section.insertBefore(newBorderElement, section.children[newIndex]);
2757
2758 }
2759 else if (bbb.borderEdit.mode === "move") { // Move the border.
2760 var oldIndex = bbb.borderEdit.index;
2761
2762 if (newIndex !== oldIndex) {
2763 var borderItem = borderSettings.splice(oldIndex, 1)[0];
2764 var borderElement = bbb.borderEdit.el;
2765
2766 if (newIndex < oldIndex)
2767 borderSettings.splice(newIndex, 0, borderItem);
2768 else if (newIndex > oldIndex)
2769 borderSettings.splice(newIndex - 1, 0, borderItem);
2770
2771 section.insertBefore(borderElement, section.children[newIndex]);
2772 }
2773 }
2774 }
2775
2776 resetBorderElements(section);
2777 section.bbbRemoveClass("bbb-insert-highlight");
2778 bbb.el.menu.window.removeEventListener("click", insertBorder, true);
2779 }
2780
2781 function showTip(event, content, styleString) {
2782 var x = event.clientX;
2783 var y = event.clientY;
2784 var tip = bbb.el.menu.tip;
2785
2786 if (styleString)
2787 tip.setAttribute("style", styleString);
2788
2789 formatTip(event, tip, content, x, y);
2790 }
2791
2792 function hideTip() {
2793 bbb.el.menu.tip.removeAttribute("style");
2794 }
2795
2796 Element.prototype.bbbBorderPreview = function(borderItem) {
2797 this.addEventListener("click", function(event) { 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;"); }, false);
2798 this.addEventListener("mouseout", hideTip, false);
2799 };
2800
2801 Element.prototype.bbbSetTip = function(text) {
2802 var tip = bbb.el.menu.tip;
2803
2804 this.addEventListener("click", function(event) {
2805 showTip(event, text, false);
2806 event.preventDefault();
2807 }, false);
2808 this.addEventListener("mouseout", function() { bbb.timers.hideTip = window.setTimeout(hideTip, 100); }, false);
2809 tip.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.hideTip); }, false);
2810 tip.addEventListener("mouseleave", hideTip, false);
2811 };
2812
2813 function changeTab(tab) {
2814 var activeTab = document.getElementsByClassName("bbb-active-tab")[0];
2815
2816 if (tab === activeTab)
2817 return;
2818
2819 activeTab.bbbRemoveClass("bbb-active-tab");
2820 bbb.el.menu[activeTab.name + "Page"].style.display = "none";
2821 bbb.el.menu.scrollDiv.scrollTop = 0;
2822 tab.bbbAddClass("bbb-active-tab");
2823 bbb.el.menu[tab.name + "Page"].style.display = "block";
2824 }
2825
2826 function tagEditWindow(input, object, prop) {
2827 var tagEditBlocker = document.createDocumentFragment();
2828
2829 var tagEditHeader = document.createElement("h2");
2830 tagEditHeader.innerHTML = "Tag Editor";
2831 tagEditHeader.className = "bbb-header";
2832 tagEditBlocker.appendChild(tagEditHeader);
2833
2834 var tagEditArea = bbb.el.menu.tagEditArea = document.createElement("textarea");
2835 tagEditArea.value = searchSingleToMulti(input.value);
2836 tagEditArea.className = "bbb-edit-area";
2837 tagEditBlocker.appendChild(tagEditArea);
2838
2839 var tagEditOk = function() {
2840 var tags = searchMultiToSingle(tagEditArea.value);
2841
2842 input.value = tags;
2843 object[prop] = tags;
2844 };
2845
2846 bbbDialog(tagEditBlocker, {ok: tagEditOk, cancel: true});
2847 }
2848
2849 function adjustMenuHeight() {
2850 var menu = bbb.el.menu.window;
2851 var scrollDiv = bbb.el.menu.scrollDiv;
2852 var viewHeight = document.documentElement.clientHeight;
2853 var scrollDivDiff = menu.offsetHeight - scrollDiv.clientHeight;
2854
2855 scrollDiv.style.maxHeight = viewHeight - scrollDiv.bbbGetPadding().height - scrollDivDiff - 50 + "px"; // Subtract 50 for margins (25 each).
2856 bbb.timers.adjustMenu = 0;
2857 }
2858
2859 function adjustMenuTimer() {
2860 if (!bbb.timers.adjustMenu && bbb.el.menu.window)
2861 bbb.timers.adjustMenu = window.setTimeout(adjustMenuHeight, 50);
2862 }
2863
2864 function removeMenu() {
2865 // Destroy the menu so that it gets rebuilt.
2866 var menu = bbb.el.menu.window;
2867
2868 if (!menu)
2869 return;
2870
2871 menu.parentNode.removeChild(menu);
2872 bbb.el.menu = {};
2873 }
2874
2875 function loadSettings() {
2876 // Load stored settings.
2877 var settings = localStorage.getItem("bbb_settings");
2878
2879 if (settings === null) {
2880 if (!getCookie().bbb_no_settings && !bbb.flags.local_storage_full) {
2881 // Alert the user when there are no settings so that new users know what to do and other users are aware their usual settings aren't in effect.
2882 var noSettingsNotice = function() {
2883 if (!getCookie().bbb_no_settings) {
2884 // Trigger the notice if it hasn't been displayed in another tab/window.
2885 var domain = location.protocol + "//" + location.hostname;
2886
2887 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);
2888 createCookie("bbb_no_settings", 1);
2889 }
2890
2891 document.removeEventListener("mousemove", noSettingsNotice, false);
2892 };
2893
2894 document.addEventListener("mousemove", noSettingsNotice, false);
2895 }
2896
2897 loadDefaults();
2898 }
2899 else {
2900 bbb.user = JSON.parse(settings);
2901 checkUser(bbb.user, bbb.options);
2902
2903 if (bbb.user.bbb_version !== bbb.options.bbb_version) {
2904 convertSettings("load");
2905 saveSettings();
2906 }
2907 }
2908 }
2909
2910 function loadDefaults() {
2911 // Load the default settings.
2912 bbb.user = {};
2913
2914 for (var i in bbb.options) {
2915 if (bbb.options.hasOwnProperty(i)) {
2916 if (typeof(bbb.options[i].def) !== "undefined")
2917 bbb.user[i] = bbb.options[i].def;
2918 else
2919 bbb.user[i] = bbb.options[i];
2920 }
2921 }
2922 }
2923
2924 function checkUser(user, options) {
2925 // Verify the user has all the base settings and add them with their default values if they don't.
2926 for (var i in options) {
2927 if (options.hasOwnProperty(i)) {
2928 if (typeof(user[i]) === "undefined") {
2929 if (typeof(options[i].def) !== "undefined")
2930 user[i] = options[i].def;
2931 else
2932 user[i] = options[i];
2933 }
2934 else if (typeof(user[i]) === "object" && !(user[i] instanceof Array))
2935 checkUser(user[i], options[i]);
2936 }
2937 }
2938 }
2939
2940 function saveSettings() {
2941 // Save the user settings to localStorage after making any necessary checks/adjustments.
2942 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.
2943 bbb.user.track_new_data = bbb.options.track_new_data.def;
2944
2945 if (bbb.settings.changed.thumb_cache_limit && thumb_cache_limit !== bbb.user.thumb_cache_limit) // Trim down the thumb cache as necessary if the limit has changed.
2946 adjustThumbCache();
2947
2948 if (bbb.settings.changed.thumbnail_count) // Update the link limit values if the user has changed the value.
2949 fixLimit(bbb.user.thumbnail_count);
2950
2951 if (bbb.settings.changed.blacklist_highlight_color && bbb.user.blacklist_highlight_color === "") // Use the default highlight color if the field is left blank.
2952 bbb.user.blacklist_highlight_color = "#CCCCCC";
2953
2954 bbb.settings.changed = {};
2955 localStorage.bbbSetItem("bbb_settings", JSON.stringify(bbb.user));
2956 }
2957
2958 function updateSettings() {
2959 // Change & save the settings without the panel. Accepts a comma delimited list of alternating settings and values: setting1, value1, setting2, value2
2960 loadSettings();
2961
2962 for (var i = 0, il = arguments.length; i < il; i += 2) {
2963 var setting = arguments[i].split(".");
2964 var value = arguments[i + 1];
2965 var settingPath = bbb.user;
2966
2967 for (var j = 0, jl = setting.length - 1; j < jl; j++)
2968 settingPath = settingPath[setting[j]];
2969
2970 settingPath[setting[j]] = value;
2971 bbb.settings.changed[setting[j]] = true;
2972 }
2973
2974 saveSettings();
2975 }
2976
2977 function convertSettings(reason) {
2978 // 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.
2979 var userVer = bbb.user.bbb_version;
2980 var scriptVer = bbb.options.bbb_version;
2981
2982 if (isOldVersion(userVer)) {
2983 switch (userVer) {
2984 case "6.0.2":
2985 // Temporary special tests for users that used the test version.
2986 if (/500$/.test(bbb.user.thumb_cache_limit))
2987 bbb.user.thumb_cache_limit = bbb.options.thumb_cache_limit.def;
2988
2989 if (!/\.(jpg|gif|png)/.test(localStorage.getItem("bbb_thumb_cache"))) {
2990 localStorage.removeItem("bbb_thumb_cache");
2991 loadThumbCache();
2992 }
2993
2994 if (bbb.user.tag_scrollbars === "false")
2995 bbb.user.tag_scrollbars = 0;
2996
2997 case "6.1":
2998 case "6.2":
2999 case "6.2.1":
3000 case "6.2.2":
3001 // Reset the thumb cache to deal with "download-preview" and incorrect extension entries.
3002 if (localStorage.getItem("bbb_thumb_cache")) {
3003 localStorage.removeItem("bbb_thumb_cache");
3004 loadThumbCache();
3005 }
3006
3007 // Convert the old hide_original_notice setting to the new show_resized_notice setting that replaces it.
3008 if (bbb.user.hide_original_notice)
3009 bbb.user.show_resized_notice = "sample";
3010
3011 // Set the new show_banned setting to true if show_deleted is true.
3012 if (bbb.user.show_deleted)
3013 bbb.user.show_banned = true;
3014
3015 // Add a custom border for banned posts to match the other hidden post borders.
3016 if (!/(?:^|\s)status:banned(?:$|\s)/i.test(JSON.stringify(bbb.user.tag_borders)))
3017 bbb.user.tag_borders.push(newBorder("status:banned", false, "#000000", "solid"));
3018
3019 // Warn about uninstalling old version from Userscripts.org
3020 if (reason !== "backup")
3021 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);
3022
3023 case "6.3":
3024 case "6.3.1":
3025 case "6.3.2":
3026 case "6.4":
3027 case "6.5":
3028 case "6.5.1":
3029 case "6.5.2":
3030 case "6.5.3":
3031 case "6.5.4":
3032 // Copy over settings to their new names.
3033 if (bbb.user.image_drag_scroll)
3034 bbb.user.post_drag_scroll = bbb.user.image_drag_scroll;
3035
3036 if (bbb.user.image_resize)
3037 bbb.user.post_resize = bbb.user.image_resize;
3038
3039 if (bbb.user.image_resize_mode)
3040 bbb.user.post_resize_mode = bbb.user.image_resize_mode;
3041
3042 if (bbb.user.tag_scrollbars)
3043 bbb.user.post_tag_scrollbars = bbb.user.tag_scrollbars;
3044
3045 // Convert old settings.
3046 if (bbb.user.autoscroll_image)
3047 bbb.user.autoscroll_post = "post";
3048
3049 if (bbb.user.search_add)
3050 bbb.user.search_add = "link";
3051
3052 if (bbb.user.override_account) {
3053 bbb.user.override_blacklist = "always";
3054 bbb.user.override_resize = true;
3055 bbb.user.override_sample = true;
3056 }
3057
3058 case "7.0":
3059 // Reduce the maximum thumb cache limit.
3060 if (bbb.user.thumb_cache_limit > 10000)
3061 bbb.user.thumb_cache_limit = 10000;
3062
3063 case "7.1":
3064 case "7.2":
3065 case "7.2.1":
3066 case "7.2.2":
3067 break;
3068 }
3069
3070 cleanUser();
3071 bbb.user.bbb_version = scriptVer;
3072 }
3073 else if (userVer !== scriptVer) // Revert the version number for downgrades so that conversion can properly work on the settings again for a future upgrade.
3074 bbb.user.bbb_version = scriptVer;
3075 }
3076
3077 function cleanUser() {
3078 // Verify the user doesn't have any settings that aren't in the base settings and delete them if they do.
3079 var user = bbb.user;
3080
3081 for (var i in user) {
3082 if (user.hasOwnProperty(i)) {
3083 if (typeof(bbb.options[i]) === "undefined")
3084 delete user[i];
3085 }
3086 }
3087 }
3088
3089 function createBackupText() {
3090 // Create a plain text version of the settings.
3091 var textarea = bbb.el.menu.backupTextarea;
3092 textarea.value = "Better Better Booru v" + bbb.user.bbb_version + " Backup (" + timestamp() + "):\r\n\r\n" + JSON.stringify(bbb.user) + "\r\n";
3093 textarea.focus();
3094 textarea.setSelectionRange(0,0);
3095 }
3096
3097 function createBackupPage() {
3098 // Open a new tab/window and place the setting text in it.
3099 window.open(('data:text/html,<!doctype html><html style="background-color: #FFFFFF;"><head><meta charset="UTF-8" /><title>Better Better Booru v' + bbb.user.bbb_version + ' Backup (' + timestamp() + ')</title></head><body style="background-color: #FFFFFF; color: #000000; padding: 20px; word-wrap: break-word;">' + JSON.stringify(bbb.user) + '</body></html>').replace(/#/g, encodeURIComponent("#")));
3100 }
3101
3102 function restoreBackupText() {
3103 // Load the backup text provided into the script.
3104 var textarea = bbb.el.menu.backupTextarea;
3105 var backupString = textarea.value.replace(/\r?\n/g, "").match(/\{.+\}/);
3106
3107 if (backupString) {
3108 try {
3109 bbb.user = JSON.parse(backupString); // This is where we expect an error.
3110 checkUser(bbb.user, bbb.options);
3111 convertSettings("backup");
3112 reloadMenu();
3113 bbbDialog("Backup settings loaded successfully. After reviewing the settings to ensure they are correct, please click \"save & close\" to finalize the restore.");
3114 }
3115 catch (error) {
3116 if (error instanceof SyntaxError)
3117 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.");
3118 else
3119 bbbDialog("Unexpected error: " + error.message);
3120 }
3121 }
3122 else
3123 bbbDialog("A backup could not be detected in the text provided. Please make sure everything was pasted correctly/completely.");
3124 }
3125
3126 /* Post functions */
3127 function swapImageInit() {
3128 // Create the custom elements for swapping between the sample and original images and set them up.
3129 createSwapElements();
3130
3131 if (image_swap_mode === "load")
3132 swapImageLoad();
3133 else if (image_swap_mode === "view")
3134 swapImageView();
3135 }
3136
3137 function createSwapElements() {
3138 // Create the elements for swapping between the original and sample image.
3139 var post = bbb.post.info;
3140
3141 if (!post.has_large)
3142 return;
3143
3144 // Remove the original notice (it's not always there) and replace it with our own.
3145 var img = document.getElementById("image");
3146 var imgContainer = document.getElementById("image-container");
3147 var resizeNotice = document.getElementById("image-resize-notice");
3148
3149 if (resizeNotice)
3150 resizeNotice.parentNode.removeChild(resizeNotice);
3151
3152 var bbbResizeNotice = bbb.el.resizeNotice = document.createElement("div");
3153 bbbResizeNotice.id = "image-resize-notice";
3154 bbbResizeNotice.className = "ui-corner-all ui-state-highlight notice notice-resized";
3155 bbbResizeNotice.style.position = "relative";
3156 bbbResizeNotice.style.display = "none";
3157 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>';
3158
3159 var resizeStatus = bbb.el.resizeStatus = getId("bbb-resize-status", bbbResizeNotice);
3160 var resizeLink = bbb.el.resizeLink = getId("bbb-resize-link", bbbResizeNotice);
3161 var closeResizeNotice = bbb.el.closeResizeNotice = getId("close-resize-notice", bbbResizeNotice);
3162
3163 closeResizeNotice.addEventListener("click", function() {
3164 var showResNot = bbb.user.show_resized_notice;
3165
3166 bbbResizeNotice.style.display = "none";
3167
3168 if (img.src.indexOf("/sample/") < 0) { // Original image.
3169 if (showResNot === "original")
3170 showResNot = "none";
3171 else if (showResNot === "all")
3172 showResNot = "sample";
3173
3174 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);
3175 }
3176 else { // Sample image.
3177 if (showResNot === "sample")
3178 showResNot = "none";
3179 else if (showResNot === "all")
3180 showResNot = "original";
3181
3182 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);
3183 }
3184
3185 updateSettings("show_resized_notice", showResNot);
3186 }, false);
3187
3188 // Create a swap image link in the sidebar options section.
3189 var optionsSectionList = document.getElementById("post-options");
3190 optionsSectionList = (optionsSectionList ? optionsSectionList.getElementsByTagName("ul")[0] : undefined);
3191
3192 var firstOption = (optionsSectionList ? optionsSectionList.getElementsByTagName("li")[0] : undefined);
3193
3194 var swapListItem = document.createElement("li");
3195
3196 var swapLink = bbb.el.swapLink = document.createElement("a");
3197 swapListItem.appendChild(swapLink);
3198
3199 swapLink.addEventListener("click", function(event) {
3200 swapPost();
3201 event.preventDefault();
3202 }, false);
3203
3204 // Prepare the element text, etc.
3205 swapImageUpdate((load_sample_first ? "sample" : "original"));
3206
3207 // Add the elements to the document.
3208 imgContainer.parentNode.insertBefore(bbbResizeNotice, imgContainer);
3209
3210 if (optionsSectionList && firstOption)
3211 optionsSectionList.insertBefore(swapListItem, firstOption);
3212 }
3213
3214 function swapImageLoad() {
3215 // Set up the post to load the content before displaying it.
3216 var post = bbb.post.info;
3217
3218 if (!post.has_large)
3219 return;
3220
3221 var img = document.getElementById("image");
3222 var bbbLoader = bbb.el.bbbLoader;
3223 var resizeStatus = bbb.el.resizeStatus;
3224 var resizeLink = bbb.el.resizeLink;
3225 var swapLink = bbb.el.swapLink;
3226
3227 resizeLink.addEventListener("click", function(event) {
3228 if (event.button === 0) {
3229 swapPost();
3230 event.preventDefault();
3231 }
3232 }, false);
3233 bbbLoader.addEventListener("load", function() { // Change the image to the successfully loaded sample/original image.
3234 if (!bbb.post.swapped)
3235 bbb.post.swapped = true;
3236
3237 if (bbbLoader.src !== "about:blank") {
3238 img.src = bbbLoader.src;
3239 bbbLoader.src = "about:blank";
3240 }
3241 }, false);
3242 bbbLoader.addEventListener("error", function(event) { // State the image has failed loading and provide a retry link.
3243 if (bbbLoader.src !== "about:blank") {
3244 var currentImg = (bbbLoader.src.indexOf("/sample/") < 0 ? "Original" : "Sample");
3245
3246 resizeStatus.innerHTML = currentImg + " image loading failed!";
3247 resizeLink.innerHTML = "retry";
3248 swapLink.innerHTML = "View " + currentImg.toLowerCase();
3249 bbbLoader.src = "about:blank";
3250 }
3251
3252 event.preventDefault();
3253 }, false);
3254 img.addEventListener("load", function() { // Update the swap image elements.
3255 if (bbbLoader.src === "about:blank") {
3256 if (img.src.indexOf("/sample/") < 0) // Original image loaded.
3257 swapImageUpdate("original");
3258 else // Sample image loaded.
3259 swapImageUpdate("sample");
3260 }
3261
3262 if (bbb.post.swapped)
3263 resizePost("swap");
3264 }, false);
3265 }
3266
3267 function swapImageView() {
3268 // Set up the post to display the content as it loads.
3269 var post = bbb.post.info;
3270
3271 if (!post.has_large)
3272 return;
3273
3274 var img = document.getElementById("image");
3275 var resizeStatus = bbb.el.resizeStatus;
3276 var resizeLink = bbb.el.resizeLink;
3277 var swapLink = bbb.el.swapLink;
3278
3279 resizeLink.addEventListener("click", function(event) {
3280 if (event.button === 0) {
3281 swapPost();
3282 event.preventDefault();
3283 }
3284 }, false);
3285 img.addEventListener("error", function(event) { // State the image has failed loading and provide a link to the other image.
3286 if (img.src !== "about:blank") {
3287 var currentImg = (img.src.indexOf("/sample/") < 0 ? "Original" : "Sample");
3288 var otherImg = (currentImg === "Original" ? "sample" : "original");
3289
3290 resizeStatus.innerHTML = currentImg + " image loading failed!";
3291 resizeLink.innerHTML = "view " + otherImg;
3292 swapLink.innerHTML = "View " + otherImg;
3293 }
3294
3295 event.preventDefault();
3296 }, false);
3297 }
3298
3299 function noteToggleInit() {
3300 // Override Danbooru's image click handler for toggling notes with a custom one.
3301 document.addEventListener("click", function(event) {
3302 if (event.target.id === "image" && event.button === 0) {
3303 if (!bbb.post.translation_mode && !bbb.drag_scroll.moved)
3304 Danbooru.Note.Box.toggle_all();
3305
3306 event.stopPropagation();
3307 }
3308 }, true);
3309 }
3310
3311 function noteToggleLinkInit() {
3312 // Make a "toggle notes" link in the sidebar options or prepare an existing link.
3313 var toggleLink = document.getElementById("bbb-note-toggle");
3314
3315 if (!toggleLink) {
3316 var before = document.getElementById((isLoggedIn() ? "add-notes-list" : "random-post"));
3317
3318 if (before) {
3319 var listNoteToggle = document.createElement("li");
3320 listNoteToggle.innerHTML = '<a href="#" id="bbb-note-toggle">Toggle notes</a>';
3321 before.parentNode.insertBefore(listNoteToggle, before);
3322 toggleLink = document.getElementById("bbb-note-toggle");
3323 }
3324 }
3325
3326 if (toggleLink) {
3327 document.getElementById("bbb-note-toggle").addEventListener("click", function(event) {
3328 Danbooru.Note.Box.toggle_all();
3329 event.preventDefault();
3330 }, false);
3331 }
3332 }
3333
3334 function translationModeInit() {
3335 // Set up translation mode.
3336 var post = bbb.post.info;
3337 var postContent = getPostContent();
3338 var postEl = postContent.el;
3339 var postTag = (postEl ? postEl.tagName : undefined);
3340 var translateLink = document.getElementById("translate");
3341 var toggleFunction;
3342
3343 if (post.file_ext !== "webm" && post.file_ext !== "mp4" && post.file_ext !== "swf") { // Don't allow translation functions on webm videos or flash.
3344 if (postTag !== "VIDEO") { // Make translation mode work on non-video content.
3345 // Allow the translation note functions if notes aren't locked.
3346 if (document.getElementById("note-locked-notice"))
3347 return;
3348
3349 // Script translation mode events and tracking used to resolve timing issues.
3350 bbb.post.translation_mode = Danbooru.Note.TranslationMode.active;
3351
3352 // Override Danbooru's toggle function which may or may not exist depending on caching.
3353 if (translateLink) {
3354 toggleFunction = function(event) {
3355 if (event.target !== translateLink)
3356 return;
3357
3358 Danbooru.Note.TranslationMode.toggle(event);
3359 translationModeToggle();
3360
3361 event.preventDefault();
3362 event.stopPropagation();
3363 };
3364
3365 translateLink.parentNode.addEventListener("click", toggleFunction, true);
3366 }
3367 }
3368 else { // Allow note viewing on ugoira webm video samples, but don't allow editing.
3369 toggleFunction = function(event) {
3370 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);
3371 event.preventDefault();
3372 };
3373
3374 Danbooru.Note.TranslationMode.toggle = toggleFunction;
3375 Danbooru.Note.Edit.show = toggleFunction;
3376
3377 if (translateLink)
3378 translateLink.addEventListener("click", toggleFunction, false);
3379
3380 // Override the hotkey for "N".
3381 createHotkey("78", toggleFunction);
3382 }
3383 }
3384 else if (translateLink) { // If the translate link exists on videos or flash, provide a warning.
3385 toggleFunction = function(event) {
3386 bbbNotice('Note editing is not allowed on flash/video content.', -1);
3387 event.preventDefault();
3388 };
3389
3390 Danbooru.Note.TranslationMode.toggle = toggleFunction;
3391 Danbooru.Note.Edit.show = toggleFunction;
3392 translateLink.addEventListener("click", toggleFunction, false);
3393 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3394 }
3395 }
3396
3397 function disableEmbeddedNotes() {
3398 // Disable embedded notes for viewing and re-enable them for note editing.
3399 var useEmbedded = (getMeta("post-has-embedded-notes") === "true");
3400 var noteContainer = document.getElementById("note-container");
3401 var notesSection = document.getElementById("notes");
3402 var notes = (notesSection ? notesSection.getElementsByTagName("article") : undefined);
3403
3404 if (!disable_embedded_notes || !useEmbedded || !notes[0])
3405 return;
3406
3407 Danbooru.Note.embed = false;
3408
3409 var post = bbb.post.info;
3410 var postContent = getPostContent();
3411 var postEl = postContent.el;
3412 var postTag = (postEl ? postEl.tagName : undefined);
3413
3414 // Stop here for content that doesn't allow note editing.
3415 if (post.file_ext === "webm" || post.file_ext === "mp4" || post.file_ext === "swf" || postTag === "VIDEO")
3416 return;
3417
3418 // Save the original note functions.
3419 var origToggleFunction = bbb.hotkeys.post[78];
3420 var origEditFunction = Danbooru.Note.Edit.show;
3421
3422 // Create override functions.
3423 var toggleFunction = function(event) {
3424 var translateLink = document.getElementById("translate");
3425
3426 if (event.type === "click" && event.target !== translateLink)
3427 return;
3428
3429 resetFunction();
3430
3431 Danbooru.Note.TranslationMode.toggle(event);
3432 translationModeToggle();
3433
3434 event.preventDefault();
3435 event.stopPropagation();
3436 };
3437
3438 var editFunction = function(editTarget) { // This function is actually assigned under an anonymous function in Danbooru. The first argument is an element from the div.
3439 resetFunction();
3440 origEditFunction(editTarget);
3441 };
3442
3443 var resetFunction = function() {
3444 // Remove all overrides/overwrites.
3445 Danbooru.Note.Edit.show = origEditFunction;
3446 document.removeEventListener("click", toggleFunction, true);
3447 createHotkey("78", origToggleFunction);
3448
3449 // Reset notes with embedded notes enabled.
3450 Danbooru.Note.embed = true;
3451 noteContainer.innerHTML = "";
3452 Danbooru.Note.load_all("bbb");
3453 };
3454
3455 document.addEventListener("click", toggleFunction, true); // Override all other click events for the translate link.
3456 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3457 Danbooru.Note.Edit.show = editFunction; // Overwrite the note edit function.
3458 }
3459
3460 function alternateImageSwap() {
3461 // Override Danbooru's image click handler for toggling notes with a custom one that swaps the image.
3462 var post = bbb.post.info;
3463
3464 if (post.has_large) {
3465 document.addEventListener("click", function(event) {
3466 if (event.target.id === "image" && event.button === 0) {
3467 if (!bbb.post.translation_mode && !bbb.drag_scroll.moved)
3468 swapPost();
3469
3470 event.stopPropagation();
3471 }
3472 }, true);
3473 }
3474
3475 // Set up the "toggle notes" link since the image won't be used for toggling.
3476 noteToggleLinkInit();
3477 }
3478
3479 function createOptionsSection() {
3480 // Create the sidebar options section for logged out users.
3481 if (isLoggedIn())
3482 return;
3483
3484 var post = bbb.post.info;
3485 var infoSection = document.getElementById("post-information");
3486 var options = document.createElement("section");
3487 options.id = "post-options";
3488 options.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><li><a href="http://danbooru.iqdb.org/db-search.php?url=http://danbooru.donmai.us' + post.preview_file_url + '">Find similar</a></li></ul>';
3489 infoSection.parentNode.insertBefore(options, infoSection.nextElementSibling);
3490 }
3491
3492 function fixPostDownloadLinks() {
3493 // Fix the "size" and "download" links in the sidebar.
3494 var post = bbb.post.info;
3495 var i, il; // Loop variables.
3496
3497 if (isLoggedIn() && !post.is_hidden)
3498 return;
3499
3500 // Fix the "size" link.
3501 var infoSection = document.getElementById("post-information");
3502
3503 if (infoSection) {
3504 var infoItems = infoSection.getElementsByTagName("li");
3505 var sizeRegex = /^(\s*Size:\s+)([\d\.]+\s+\S+)/i;
3506
3507 for (i = 0, il = infoItems.length; i < il; i++) {
3508 var infoItem = infoItems[i];
3509
3510 if (sizeRegex.test(infoItem.innerHTML)) {
3511 infoItem.innerHTML = infoItem.innerHTML.replace(sizeRegex, '$1<a href="' + post.file_url + '">$2</a>');
3512 break;
3513 }
3514 }
3515 }
3516
3517 // Fix the "download" link.
3518 var optionsSection = document.getElementById("post-options");
3519
3520 if (optionsSection) {
3521 var optionItems = optionsSection.getElementsByTagName("li");
3522 var downloadRegex = /^\s*Download\s*$/i;
3523 var title = getMeta("og:title");
3524 var downloadName = (title ? title.replace(" - Danbooru", " - ") : "");
3525
3526 for (i = 0, il = optionItems.length; i < il; i++) {
3527 var optionItem = optionItems[i];
3528
3529 if (downloadRegex.test(optionItem.innerHTML)) {
3530 optionItem.innerHTML = '<a download="' + downloadName + post.md5 + '.' + post.file_ext + '" href="' + post.file_url + '">Download</a>';
3531 break;
3532 }
3533 }
3534 }
3535 }
3536
3537 function modifyResizeLink() {
3538 // Replace the single resize link with three custom resize links.
3539 var resizeListLink = document.getElementById("image-resize-to-window-link");
3540
3541 if (!resizeListLink)
3542 return;
3543
3544 var resizeListItem = resizeListLink.parentNode;
3545 var resizeListParent = resizeListItem.parentNode;
3546 var optionsFrag = document.createDocumentFragment();
3547
3548 var resizeLinkAll = bbb.el.resizeLinkAll = document.createElement("a");
3549 resizeLinkAll.href = "#";
3550 resizeLinkAll.addEventListener("click", function(event) {
3551 resizePost("all");
3552 event.preventDefault();
3553 }, false);
3554
3555 var resizeLinkWidth = bbb.el.resizeLinkWidth = document.createElement("a");
3556 resizeLinkWidth.href = "#";
3557 resizeLinkWidth.addEventListener("click", function(event) {
3558 resizePost("width");
3559 event.preventDefault();
3560 }, false);
3561
3562 var resizeLinkHeight = bbb.el.resizeLinkHeight = document.createElement("a");
3563 resizeLinkHeight.href = "#";
3564 resizeLinkHeight.addEventListener("click", function(event) {
3565 resizePost("height");
3566 event.preventDefault();
3567 }, false);
3568
3569 if (resize_link_style === "full") {
3570 var resizeListAll = document.createElement("li");
3571 optionsFrag.appendChild(resizeListAll);
3572
3573 resizeLinkAll.innerHTML = "Resize to window";
3574 resizeListAll.appendChild(resizeLinkAll);
3575
3576 var resizeListWidth = document.createElement("li");
3577 optionsFrag.appendChild(resizeListWidth);
3578
3579 resizeLinkWidth.innerHTML = "Resize to window width";
3580 resizeListWidth.appendChild(resizeLinkWidth);
3581
3582 var resizeListHeight = document.createElement("li");
3583 optionsFrag.appendChild(resizeListHeight);
3584
3585 resizeLinkHeight.innerHTML = "Resize to window height";
3586 resizeListHeight.appendChild(resizeLinkHeight);
3587
3588 resizeListParent.replaceChild(optionsFrag, resizeListItem);
3589 }
3590 else if (resize_link_style === "minimal") {
3591 var resizeList = document.createElement("li");
3592 optionsFrag.appendChild(resizeList);
3593
3594 var resizeLabelLink = document.createElement("a");
3595 resizeLabelLink.href = "#";
3596 resizeLabelLink.innerHTML = "Resize:";
3597 resizeLabelLink.style.marginRight = "2px";
3598 resizeLabelLink.addEventListener("click", function(event) {
3599 if (bbb.post.resize.mode === "none")
3600 resizePost("all");
3601 else
3602 resizePost("none");
3603
3604 event.preventDefault();
3605 }, false);
3606 resizeList.appendChild(resizeLabelLink);
3607
3608 resizeLinkAll.innerHTML = "(W&H)";
3609 resizeLinkAll.className = "bbb-resize-link";
3610 resizeLinkAll.title = "Resize to Window Width & Height";
3611 resizeList.appendChild(resizeLinkAll);
3612
3613 resizeLinkWidth.innerHTML = "(W)";
3614 resizeLinkWidth.className = "bbb-resize-link";
3615 resizeLinkWidth.title = "Resize to Window Width";
3616 resizeList.appendChild(resizeLinkWidth);
3617
3618 resizeLinkHeight.innerHTML = "(H)";
3619 resizeLinkHeight.className = "bbb-resize-link";
3620 resizeLinkHeight.title = "Resize to Window Height";
3621 resizeList.appendChild(resizeLinkHeight);
3622
3623 resizeList.style.height = "0px";
3624 resizeList.style.visibility = "hidden";
3625 resizeList.style.fontWeight = "bold";
3626 resizeList.style.position = "relative";
3627
3628 resizeListParent.insertBefore(resizeList, resizeListItem);
3629
3630 var allWidth = resizeLinkAll.clientWidth;
3631 var widthWidth = resizeLinkWidth.clientWidth;
3632 var heightWidth = resizeLinkHeight.clientWidth;
3633
3634 resizeLinkAll.style.width = allWidth + "px";
3635 resizeLinkWidth.style.width = widthWidth + "px";
3636 resizeLinkHeight.style.width = heightWidth + "px";
3637
3638 resizeList.style.height = "auto";
3639 resizeList.style.visibility = "visible";
3640 resizeList.style.fontWeight = "normal";
3641
3642 resizeListParent.removeChild(resizeListItem);
3643 }
3644 }
3645
3646 function resizePost(mode) {
3647 // Custom resize post script.
3648 var postContent = getPostContent();
3649 var imgContainer = postContent.container;
3650 var contentDiv = document.getElementById("content");
3651 var ugoiraPanel = document.getElementById("ugoira-control-panel");
3652 var ugoiraSlider = document.getElementById("seek-slider");
3653 var target = postContent.el;
3654 var targetTag = (target ? target.tagName : undefined);
3655
3656 if (!target || !imgContainer || !contentDiv || targetTag === "A")
3657 return;
3658
3659 var currentMode = bbb.post.resize.mode;
3660 var currentRatio = bbb.post.resize.ratio;
3661 var resizeLinkAll = bbb.el.resizeLinkAll;
3662 var resizeLinkWidth = bbb.el.resizeLinkWidth;
3663 var resizeLinkHeight = bbb.el.resizeLinkHeight;
3664 var availableWidth = imgContainer.clientWidth || contentDiv.clientWidth - contentDiv.bbbGetPadding().width;
3665 var availableHeight = document.documentElement.clientHeight - 40;
3666 var targetCurrentWidth = target.clientWidth || parseFloat(target.style.width) || target.getAttribute("width");
3667 var targetCurrentHeight = target.clientHeight || parseFloat(target.style.height) || target.getAttribute("height");
3668 var useDataDim = targetTag === "EMBED" || targetTag === "VIDEO";
3669 var targetWidth = (useDataDim ? imgContainer.getAttribute("data-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...
3670 var targetHeight = (useDataDim ? imgContainer.getAttribute("data-height") : target.getAttribute("height"));
3671 var tooWide = targetCurrentWidth > availableWidth;
3672 var tooTall = targetCurrentHeight > availableHeight;
3673 var widthRatio = availableWidth / targetWidth;
3674 var heightRatio = availableHeight / targetHeight;
3675 var imgMode = mode;
3676 var switchMode = false;
3677 var ratio = 1;
3678 var linkWeight = {all: "normal", width: "normal", height: "normal"};
3679
3680 if (mode === "swap") { // The image is being swapped between the original and sample image so everything needs to be reset. Ignore the current mode.
3681 switchMode = true;
3682 imgMode = "none";
3683 }
3684 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.
3685 if (currentMode !== "none") { // No need to do anything if the content is already at the original dimensions.
3686 switchMode = true;
3687 imgMode = "none";
3688 }
3689 }
3690 else if (mode === "height" && (tooTall || currentMode !== "none")) {
3691 switchMode = true;
3692 ratio = heightRatio;
3693 linkWeight.height = "bold";
3694 }
3695 else if (mode === "width" && (tooWide || currentMode !== "none")) {
3696 switchMode = true;
3697 ratio = widthRatio;
3698 linkWeight.width = "bold";
3699 }
3700 else if (mode === "all" && (tooWide || tooTall || currentMode !== "none")) {
3701 switchMode = true;
3702 ratio = (widthRatio < heightRatio ? widthRatio : heightRatio);
3703 linkWeight.all = "bold";
3704 }
3705
3706 if (switchMode) {
3707 if (currentRatio !== ratio || mode === "swap") {
3708 if (targetTag === "IMG" || targetTag === "CANVAS") {
3709 target.style.width = targetWidth * ratio + "px";
3710 target.style.height = targetHeight * ratio + "px";
3711
3712 if (ugoiraPanel && ugoiraSlider) {
3713 ugoiraPanel.style.width = targetWidth * ratio + "px";
3714 ugoiraSlider.style.width = targetWidth * ratio - 81 + "px";
3715 }
3716
3717 Danbooru.Note.Box.scale_all();
3718 }
3719 else if (targetTag === "EMBED") {
3720 var secondaryTarget = postContent.secEl;
3721
3722 secondaryTarget.height = target.height = targetHeight * ratio;
3723 secondaryTarget.width = target.width = targetWidth * ratio;
3724 }
3725 else if (targetTag === "VIDEO") {
3726 target.height = targetHeight * ratio;
3727 target.width = targetWidth * ratio;
3728 }
3729 }
3730
3731 bbb.post.resize.mode = imgMode;
3732 bbb.post.resize.ratio = ratio;
3733 resizeLinkAll.style.fontWeight = linkWeight.all;
3734 resizeLinkWidth.style.fontWeight = linkWeight.width;
3735 resizeLinkHeight.style.fontWeight = linkWeight.height;
3736 }
3737 }
3738
3739 function swapPost() {
3740 // Initiate the swap between the sample and original post content.
3741 var post = bbb.post.info;
3742 var target = getPostContent().el;
3743 var targetTag = (target ? target.tagName : undefined);
3744 var bbbLoader = bbb.el.bbbLoader;
3745 var resizeStatus = bbb.el.resizeStatus;
3746 var resizeLink = bbb.el.resizeLink;
3747 var swapLink = bbb.el.swapLink;
3748
3749 if (!post.has_large)
3750 return;
3751
3752 if (post.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(post.tag_string)) {
3753 if (targetTag === "CANVAS")
3754 location.href = updateURLQuery(location.href, {original: "0"});
3755 else if (targetTag === "VIDEO")
3756 location.href = updateURLQuery(location.href, {original: "1"});
3757 }
3758 else if (targetTag === "IMG") {
3759 if (image_swap_mode === "load") { // Load image and then view mode.
3760 if (bbbLoader.src !== "about:blank") { // Messages after cancelling.
3761 if (target.src.indexOf("/sample/") < 0)
3762 swapImageUpdate("original");
3763 else
3764 swapImageUpdate("sample");
3765
3766 bbbLoader.src = "about:blank";
3767 }
3768 else { // Messages during loading.
3769 if (target.src.indexOf("/sample/") < 0) {
3770 resizeStatus.innerHTML = "Loading sample image...";
3771 resizeLink.innerHTML = "cancel";
3772 swapLink.innerHTML = "View sample (cancel)";
3773 bbbLoader.src = post.large_file_url;
3774 }
3775 else {
3776 resizeStatus.innerHTML = "Loading original image...";
3777 resizeLink.innerHTML = "cancel";
3778 swapLink.innerHTML = "View original (cancel)";
3779 bbbLoader.src = post.file_url;
3780 }
3781 }
3782 }
3783 else if (image_swap_mode === "view") { // View image while loading mode.
3784 if (target.src.indexOf("/sample/") < 0) { // Load the sample image.
3785 swapImageUpdate("sample");
3786 target.src = "about:blank";
3787 target.removeAttribute("src");
3788 delayMe(function() { target.src = post.large_file_url; });
3789 }
3790 else { // Load the original image.
3791 swapImageUpdate("original");
3792 target.src = "about:blank";
3793 target.removeAttribute("src");
3794 delayMe(function() { target.src = post.file_url; });
3795 }
3796
3797 if (!bbb.post.swapped)
3798 delayMe(function() { resizePost("swap"); });
3799 else
3800 bbb.post.swapped = true;
3801 }
3802 }
3803 }
3804
3805 function swapImageUpdate(mode) {
3806 // Update all the elements related to swapping images when the image URL is changed.
3807 var post = bbb.post.info;
3808 var img = document.getElementById("image");
3809 var bbbResizeNotice = bbb.el.resizeNotice;
3810 var resizeStatus = bbb.el.resizeStatus;
3811 var resizeLink = bbb.el.resizeLink;
3812 var swapLink = bbb.el.swapLink;
3813 var showResNot = bbb.user.show_resized_notice;
3814
3815 if (mode === "original") { // When the image is changed to the original image.
3816 resizeStatus.innerHTML = "Viewing original";
3817 resizeLink.innerHTML = "view sample";
3818 resizeLink.href = post.large_file_url;
3819 swapLink.innerHTML = "View sample";
3820 swapLink.href = post.large_file_url;
3821 img.alt = post.md5;
3822 img.setAttribute("height", post.image_height);
3823 img.setAttribute("width", post.image_width);
3824 bbbResizeNotice.style.display = (showResNot === "original" || showResNot === "all" ? "block" : "none");
3825 }
3826 else if (mode === "sample") { // When the image is changed to the sample image.
3827 resizeStatus.innerHTML = "Resized to " + Math.floor(post.sample_ratio * 100) + "% of original";
3828 resizeLink.innerHTML = "view original";
3829 resizeLink.href = post.file_url;
3830 swapLink.innerHTML = "View original";
3831 swapLink.href = post.file_url;
3832 img.alt = "Sample";
3833 img.setAttribute("height", post.sample_height);
3834 img.setAttribute("width", post.sample_width);
3835 bbbResizeNotice.style.display = (showResNot === "sample" || showResNot === "all" ? "block" : "none");
3836 }
3837 }
3838
3839 function checkRelations() {
3840 // Test whether the parent/child notice could have hidden posts.
3841 var post = bbb.post.info;
3842 var thumbCount;
3843 var deletedCount;
3844 var loggedIn = isLoggedIn();
3845 var fixParent = false;
3846 var fixChild = false;
3847 var relationCookie = getCookie()["show-relationship-previews"];
3848 var showPreview = (relationCookie === undefined || relationCookie === "1" ? true : false);
3849 var parentLink = document.getElementById("has-children-relationship-preview-link");
3850 var childLink = document.getElementById("has-parent-relationship-preview-link");
3851
3852 if (post.has_children) {
3853 var parentNotice = document.getElementsByClassName("notice-parent")[0];
3854
3855 if (parentNotice) {
3856 var parentText = parentNotice.textContent.match(/has (\d+|a) child/);
3857 var parentCount = (parentText ? Number(parentText[1]) || 1 : 0);
3858 thumbCount = getPosts(parentNotice).length;
3859 deletedCount = parentNotice.getElementsByClassName("post-status-deleted").length;
3860
3861 if ((!loggedIn && show_deleted && !deletedCount) || (parentCount && parentCount + 1 !== thumbCount))
3862 fixParent = true;
3863 }
3864 else if (show_deleted)
3865 fixParent = true;
3866 }
3867
3868 if (fixParent) {
3869 if (showPreview || !parentLink)
3870 searchJSON("parent", post.id);
3871 else
3872 parentLink.addEventListener("click", requestRelations, false);
3873 }
3874
3875 if (post.parent_id) {
3876 var childNotice = document.getElementsByClassName("notice-child")[0];
3877
3878 if (childNotice) {
3879 var childText = childNotice.textContent.match(/has (\d+|a) sibling/);
3880 var childCount = (childText ? Number(childText[1]) || 1 : 0) + 1;
3881 thumbCount = getPosts(childNotice).length;
3882 deletedCount = childNotice.getElementsByClassName("post-status-deleted").length;
3883
3884 if ((!loggedIn && show_deleted && !deletedCount) || (childCount && childCount + 1 !== thumbCount))
3885 fixChild = true;
3886 }
3887 }
3888
3889 if (fixChild) {
3890 if (showPreview || !childLink)
3891 searchJSON("child", post.parent_id);
3892 else
3893 childLink.addEventListener("click", requestRelations, false);
3894 }
3895 }
3896
3897 function requestRelations(event) {
3898 // Start the parent/child notice JSON request when the user chooses to display the thumbs in a notice.
3899 var post = bbb.post.info;
3900 var target = event.target;
3901
3902 if (target.id === "has-children-relationship-preview-link")
3903 searchJSON("parent", post.id);
3904 else if (target.id === "has-parent-relationship-preview-link")
3905 searchJSON("child", post.parent_id);
3906
3907 target.removeEventListener("click", requestRelations, false);
3908 event.preventDefault();
3909 }
3910
3911 function removeTagHeaders() {
3912 // Remove the "copyright", "characters", and "artist" headers in the post sidebar.
3913 var tagList = document.getElementById("tag-list");
3914
3915 if (!tagList || !remove_tag_headers || gLoc !== "post")
3916 return;
3917
3918 var tagHolder = document.createDocumentFragment();
3919 var childIndex = 0;
3920 var mainList;
3921
3922 while (tagList.children[childIndex]) {
3923 var header = tagList.children[childIndex];
3924 var list = tagList.children[childIndex + 1];
3925
3926 if (header.tagName === "H2" && list && list.tagName === "UL") {
3927 tagList.removeChild(header);
3928 tagList.removeChild(list);
3929
3930 while (list.firstElementChild)
3931 tagHolder.appendChild(list.firstElementChild);
3932 }
3933 else if (header.tagName === "H1" && list && list.tagName === "UL") {
3934 mainList = list;
3935 childIndex += 2;
3936 }
3937 else
3938 childIndex++;
3939 }
3940
3941 if (mainList)
3942 mainList.insertBefore(tagHolder, mainList.firstElementChild);
3943 else {
3944 var newHeader = document.createElement("h1");
3945 newHeader.innerHTML = "Tags";
3946 tagList.appendChild(newHeader);
3947
3948 var newList = document.createElement("ul");
3949 newList.appendChild(tagHolder);
3950 tagList.appendChild(newList);
3951 }
3952 }
3953
3954 function postTagTitles() {
3955 // Replace the post title with the full set of tags.
3956 if (post_tag_titles && gLoc === "post")
3957 document.title = getMeta("tags").replace(/\s/g, ", ").replace(/_/g, " ") + " - Danbooru";
3958 }
3959
3960 function minimizeStatusNotices() {
3961 // Show status notices only when their respective status link is clicked in the sidebar.
3962 if (!minimize_status_notices || gLoc !== "post")
3963 return;
3964
3965 var infoSection = document.getElementById("post-information");
3966 var infoListItems = (infoSection ? infoSection.getElementsByTagName("li") : null);
3967 var statusListItem;
3968 var newStatusContent;
3969 var flaggedNotice = document.getElementsByClassName("notice-flagged")[0];
3970 var appealedNotice = document.getElementsByClassName("notice-appealed")[0];
3971 var pendingNotice = document.getElementsByClassName("notice-pending")[0];
3972 var deletedNotices = document.getElementsByClassName("notice-deleted");
3973 var deletedNotice;
3974 var bannedNotice;
3975 var i, il; // Loop variables.
3976
3977 if (infoListItems) {
3978 // Locate the status portion of the information section.
3979 for (i = infoListItems.length - 1; i >= 0; i--) {
3980 var infoListItem = infoListItems[i];
3981
3982 if (infoListItem.textContent.indexOf("Status:") > -1) {
3983 statusListItem = infoListItem;
3984 newStatusContent = statusListItem.textContent;
3985 break;
3986 }
3987 }
3988
3989 // Hide and alter the notices and create the appropriate status links.
3990 if (statusListItem) {
3991 if (flaggedNotice) {
3992 flaggedNotice.style.display = "none";
3993 flaggedNotice.style.position = "absolute";
3994 flaggedNotice.style.zIndex = "2003";
3995 newStatusContent = newStatusContent.replace("Flagged", '<a href="#" id="bbb-flagged-link">Flagged</a>');
3996 }
3997
3998 if (pendingNotice) {
3999 pendingNotice.style.display = "none";
4000 pendingNotice.style.position = "absolute";
4001 pendingNotice.style.zIndex = "2003";
4002 newStatusContent = newStatusContent.replace("Pending", '<a href="#" id="bbb-pending-link">Pending</a>');
4003 }
4004
4005 for (i = 0, il = deletedNotices.length; i < il; i++) {
4006 deletedNotices[i].style.display = "none";
4007 deletedNotices[i].style.position = "absolute";
4008 deletedNotices[i].style.zIndex = "2003";
4009
4010 if (deletedNotices[i].getElementsByTagName("li")[0]) {
4011 deletedNotice = deletedNotices[i];
4012 newStatusContent = newStatusContent.replace("Deleted", '<a href="#" id="bbb-deleted-link">Deleted</a>');
4013 }
4014 else {
4015 bannedNotice = deletedNotices[i];
4016 newStatusContent = newStatusContent.replace("Banned", '<a href="#" id="bbb-banned-link">Banned</a>');
4017 }
4018 }
4019
4020 if (appealedNotice) {
4021 appealedNotice.style.display = "none";
4022 appealedNotice.style.position = "absolute";
4023 appealedNotice.style.zIndex = "2003";
4024 newStatusContent = newStatusContent + ' <a href="#" id="bbb-appealed-link">Appealed</a>';
4025 }
4026
4027 statusListItem.innerHTML = newStatusContent;
4028 }
4029
4030 // Prepare the links.
4031 var flaggedLink = document.getElementById("bbb-flagged-link");
4032 var appealedLink = document.getElementById("bbb-appealed-link");
4033 var pendingLink = document.getElementById("bbb-pending-link");
4034 var deletedLink = document.getElementById("bbb-deleted-link");
4035 var bannedLink = document.getElementById("bbb-banned-link");
4036
4037 if (flaggedLink)
4038 statusLinkEvents(flaggedLink, flaggedNotice);
4039 if (appealedLink)
4040 statusLinkEvents(appealedLink, appealedNotice);
4041 if (pendingLink)
4042 statusLinkEvents(pendingLink, pendingNotice);
4043 if (deletedLink)
4044 statusLinkEvents(deletedLink, deletedNotice);
4045 if (bannedLink)
4046 statusLinkEvents(bannedLink, bannedNotice);
4047 }
4048 }
4049
4050 function statusLinkEvents(link, notice) {
4051 // Attach events to the status links to enable a tooltip style notice.
4052 link.addEventListener("click", function(event) { showStatusNotice(event, notice); }, false);
4053 link.addEventListener("mouseout", function() { bbb.timers.minNotice = window.setTimeout(function() { notice.style.display = "none"; }, 200); }, false);
4054 notice.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.minNotice); }, false);
4055 notice.addEventListener("mouseleave", function() { notice.style.display = "none"; }, false);
4056 }
4057
4058 function showStatusNotice(event, noticeEl) {
4059 // Display a minimized status notice upon a click event.
4060 var x = event.pageX;
4061 var y = event.pageY;
4062 var notice = noticeEl;
4063 var topOffset = 0;
4064
4065 notice.style.maxWidth = document.documentElement.clientWidth * 0.66 + "px";
4066 notice.style.visibility = "hidden";
4067 notice.style.display = "block";
4068
4069 // Don't allow the notice to go above the top of the window.
4070 if (event.clientY - notice.offsetHeight - 2 < 5)
4071 topOffset = event.clientY - notice.offsetHeight - 7;
4072
4073 notice.style.left = x + 2 + "px";
4074 notice.style.top = y - notice.offsetHeight - 2 - topOffset + "px";
4075 notice.style.visibility = "visible";
4076
4077 event.preventDefault();
4078 }
4079
4080 function dragScrollInit() {
4081 // Start up drag scroll.
4082 if (!post_drag_scroll)
4083 return;
4084
4085 var target = getPostContent().el;
4086 var targetTag = (target ? target.tagName : undefined);
4087
4088 if (targetTag === "IMG" || targetTag === "VIDEO" || targetTag === "CANVAS") {
4089 bbb.drag_scroll.target = target;
4090
4091 if (!bbb.post.translation_mode)
4092 dragScrollEnable();
4093
4094 // Disable click behavior when dragging the video around.
4095 if (targetTag === "VIDEO") {
4096 document.addEventListener("click", function(event) {
4097 if (event.button === 0 && event.target.id === "image" && bbb.drag_scroll.moved)
4098 event.preventDefault();
4099 }, true);
4100 }
4101 }
4102 }
4103
4104 function dragScrollToggle() {
4105 // Enable drag scroll with translation mode is off and disable it when translation mode is on.
4106 if (!post_drag_scroll || !bbb.drag_scroll.target)
4107 return;
4108
4109 if (bbb.post.translation_mode)
4110 dragScrollDisable();
4111 else
4112 dragScrollEnable();
4113 }
4114
4115 function dragScrollEnable() {
4116 // Add the drag scroll event listeners.
4117 var target = bbb.drag_scroll.target;
4118
4119 target.addEventListener("mousedown", dragScrollOn, false);
4120 target.addEventListener("dragstart", disableEvent, false);
4121 target.addEventListener("selectstart", disableEvent, false);
4122 }
4123
4124 function dragScrollDisable() {
4125 // Remove the drag scroll event listeners.
4126 var target = bbb.drag_scroll.target;
4127
4128 target.removeEventListener("mousedown", dragScrollOn, false);
4129 target.removeEventListener("dragstart", disableEvent, false);
4130 target.removeEventListener("selectstart", disableEvent, false);
4131 }
4132
4133 function dragScrollOn(event) {
4134 // Start monitoring mouse movement.
4135 if (event.button === 0) {
4136 bbb.drag_scroll.lastX = event.clientX;
4137 bbb.drag_scroll.lastY = event.clientY;
4138 bbb.drag_scroll.moved = false;
4139
4140 document.addEventListener("mousemove", dragScrollMove, false);
4141 document.addEventListener("mouseup", dragScrollOff, false);
4142 }
4143 }
4144
4145 function dragScrollMove(event) {
4146 // Move the page based on mouse movement.
4147 var newX = event.clientX;
4148 var newY = event.clientY;
4149 var xDistance = bbb.drag_scroll.lastX - newX;
4150 var yDistance = bbb.drag_scroll.lastY - newY;
4151
4152 window.scrollBy(xDistance, yDistance);
4153
4154 bbb.drag_scroll.lastX = newX;
4155 bbb.drag_scroll.lastY = newY;
4156 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.
4157 }
4158
4159 function dragScrollOff() {
4160 // Stop monitoring mouse movement.
4161 document.removeEventListener("mousemove", dragScrollMove, false);
4162 document.removeEventListener("mouseup", dragScrollOff, false);
4163 }
4164
4165 function disableEvent(event) {
4166 // removeEventListener friendly function for stopping an event.
4167 event.preventDefault();
4168 }
4169
4170 function translationModeToggle() {
4171 // Toggle the translation mode status and drag scrolling.
4172 bbb.post.translation_mode = !bbb.post.translation_mode;
4173
4174 dragScrollToggle();
4175 }
4176
4177 function autoscrollPost() {
4178 // Automatically scroll a post to the desired position.
4179 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
4180
4181 if (autoscroll_post === "none" || scrolled !== 0) // Don't scroll if the page is already srolled.
4182 return;
4183
4184 if (autoscroll_post === "post") {
4185 var target = getPostContent().el;
4186
4187 if (target)
4188 target.scrollIntoView();
4189 }
4190 else if (autoscroll_post === "header") {
4191 var page = document.getElementById("page");
4192
4193 if (!page)
4194 return;
4195
4196 var pageTop = page.offsetTop;
4197
4198 window.scroll(0, pageTop);
4199 }
4200 }
4201
4202 function ugoiraInit() {
4203 // Execute a static copy of Danbooru's embedded JavaScript for setting up the post.
4204 var post = bbb.post.info;
4205
4206 try {
4207 Danbooru.Ugoira = {};
4208
4209 Danbooru.Ugoira.create_player = function() {
4210 var meta_data = {
4211 mime_type: post.pixiv_ugoira_frame_data.content_type,
4212 frames: post.pixiv_ugoira_frame_data.data
4213 };
4214 var options = {
4215 canvas: document.getElementById("image"),
4216 source: post.file_url,
4217 metadata: meta_data,
4218 chunkSize: 300000,
4219 loop: true,
4220 autoStart: true,
4221 debug: false
4222 };
4223
4224 this.player = new ZipImagePlayer(options);
4225 };
4226
4227 Danbooru.Ugoira.player = null;
4228
4229 $(function() {
4230 Danbooru.Ugoira.create_player();
4231 $(Danbooru.Ugoira.player).on("loadProgress", function(event, progress) {
4232 $("#seek-slider").progressbar("value", Math.floor(progress * 100));
4233 });
4234
4235 var player_manually_paused = false;
4236
4237 $("#ugoira-play").click(function(event) {
4238 Danbooru.Ugoira.player.play();
4239 $(this).hide();
4240 $("#ugoira-pause").show();
4241 player_manually_paused = false;
4242 event.preventDefault();
4243 });
4244 $("#ugoira-pause").click(function(event) {
4245 Danbooru.Ugoira.player.pause();
4246 $(this).hide();
4247 $("#ugoira-play").show();
4248 player_manually_paused = true;
4249 event.preventDefault();
4250 });
4251
4252 $("#seek-slider").progressbar({
4253 value: 0
4254 });
4255
4256 $("#seek-slider").slider({
4257 min: 0,
4258 max: Danbooru.Ugoira.player._frameCount-1,
4259 start: function() {
4260 // Need to pause while slider is being dragged or playback speed will bug out
4261 Danbooru.Ugoira.player.pause();
4262 },
4263 slide: function(event, ui) {
4264 Danbooru.Ugoira.player._frame = ui.value;
4265 Danbooru.Ugoira.player._displayFrame();
4266 },
4267 stop: function() {
4268 // Resume playback when dragging stops, but only if player was not paused by the user earlier
4269 if (!(player_manually_paused)) {
4270 Danbooru.Ugoira.player.play();
4271 }
4272 }
4273 });
4274 $(Danbooru.Ugoira.player).on("frame", function(frame, frame_number) {
4275 $("#seek-slider").slider("option", "value", frame_number);
4276 });
4277 });
4278 }
4279 catch (error) {
4280 bbbNotice("Unexpected error creating the ugoira post. (Error: " + error.message + ")", -1);
4281 }
4282 }
4283
4284 /* Thumbnail functions */
4285 function formatThumbnails(target) {
4286 // Create thumbnail titles and borders.
4287 var posts = getPosts(target);
4288 var i, il; // Loop variables.
4289
4290 if (!posts[0])
4291 return;
4292
4293 var searches = bbb.custom_tag.searches;
4294
4295 // Create and cache border search objects.
4296 if (custom_tag_borders && !searches[0]) {
4297 for (i = 0, il = tag_borders.length; i < il; i++)
4298 searches.push(createSearch(tag_borders[i].tags));
4299 }
4300
4301 // Cycle through each post and apply titles and borders.
4302 for (i = 0, il = posts.length; i < il; i++) {
4303 var post = posts[i];
4304 var img = post.getElementsByTagName("img")[0];
4305
4306 if (!img)
4307 continue;
4308
4309 var link = img.parentNode;
4310 var tags = post.getAttribute("data-tags");
4311 var tagsStr = (tags ? tags : "");
4312 var user = post.getAttribute("data-uploader");
4313 var userStr = (user ? " user:" + user : "");
4314 var rating = post.getAttribute("data-rating");
4315 var ratingStr = (rating ? " rating:" + rating : "");
4316 var score = post.getAttribute("data-score");
4317 var scoreStr = (score ? " score:" + score : "");
4318 var views = post.getAttribute("data-views");
4319 var viewsStr = (views ? " views:" + views : "");
4320 var title = tagsStr + userStr + ratingStr + scoreStr + viewsStr;
4321 var id = post.getAttribute("data-id");
4322 var hasChildren = (post.getAttribute("data-has-children") === "true" ? true : false);
4323 var secondary = [];
4324 var secondaryLength = 0;
4325 var borderStyle;
4326 var styleList = bbb.custom_tag.style_list;
4327
4328 // Skip thumbnails that have already been done.
4329 if (link.bbbHasClass("bbb-thumb-link"))
4330 continue;
4331
4332 // Create title.
4333 img.title = title;
4334
4335 // Give the thumbnail link an identifying class.
4336 link.bbbAddClass("bbb-thumb-link");
4337
4338 // Give the post container an ID class for resolving cases where the same post shows up on the page multiple times.
4339 post.bbbAddClass("post_" + id);
4340
4341 // Correct parent status borders on "no active children" posts for logged out users.
4342 if (hasChildren && show_deleted)
4343 post.bbbAddClass("post-status-has-children");
4344
4345 // Secondary custom tag borders.
4346 if (custom_tag_borders) {
4347 if (typeof(styleList[id]) === "undefined") {
4348 for (var j = 0, jl = tag_borders.length; j < jl; j++) {
4349 var tagBorderItem = tag_borders[j];
4350
4351 if (tagBorderItem.is_enabled && thumbSearchMatch(post, searches[j])) {
4352 secondary.push([tagBorderItem.border_color, tagBorderItem.border_style]);
4353
4354 if (secondary.length === 4)
4355 break;
4356 }
4357 }
4358
4359 secondaryLength = secondary.length;
4360
4361 if (secondaryLength) {
4362 link.bbbAddClass("bbb-custom-tag");
4363
4364 if (secondaryLength === 1 || (single_color_borders && secondaryLength > 1))
4365 borderStyle = "border-color: " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " !important;";
4366 else if (secondaryLength === 2)
4367 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;";
4368 else if (secondaryLength === 3)
4369 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;";
4370 else if (secondaryLength === 4)
4371 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;";
4372
4373 link.setAttribute("style", borderStyle);
4374 styleList[id] = borderStyle;
4375 }
4376 else
4377 styleList[id] = false;
4378 }
4379 else if (styleList[id] !== false && !post.bbbHasClass("bbb-custom-tag")) { // Post is already tested, but needs to be set up again.
4380 link.bbbAddClass("bbb-custom-tag");
4381 link.setAttribute("style", styleList[id]);
4382 }
4383 }
4384 }
4385 }
4386
4387 function prepThumbnails(target) {
4388 // Take new thumbnails and apply the necessary functions for preparing them.
4389 // Thumbnail classes and titles.
4390 formatThumbnails(target);
4391
4392 // Thumbnail info.
4393 thumbInfo(target);
4394
4395 // Clean links.
4396 cleanLinks(target);
4397
4398 // Blacklist.
4399 blacklistUpdate(target);
4400
4401 // Direct downloads.
4402 postDDL(target);
4403
4404 // Quick search.
4405 quickSearchTest(target);
4406
4407 // Fix the mode menu.
4408 danbModeMenu(target);
4409 }
4410
4411 function checkHiddenThumbs(post) {
4412 // Alter a hidden thumbnails with cache info or queue it for the cache.
4413 if (!post.md5) {
4414 if (!bbb.cache.stored.history)
4415 loadThumbCache();
4416
4417 var cacheName = bbb.cache.stored.names[post.id];
4418
4419 if (cacheName) { // Load the thumbnail info from the cache.
4420 if (cacheName === "download-preview.png")
4421 post.preview_file_url = "/images/download-preview.png";
4422 else {
4423 var cacheValues = cacheName.split(".");
4424 var cacheMd5 = cacheValues[0];
4425 var cacheExt = cacheValues[1];
4426
4427 post.md5 = cacheMd5;
4428 post.file_ext = cacheExt;
4429 post.preview_file_url = (!post.image_height || cacheExt === "swf" ? "/images/download-preview.png" : "/data/preview/" + cacheMd5 + ".jpg");
4430 post.large_file_url = (post.has_large ? "/data/sample/sample-" + cacheMd5 + ".jpg" : "/data/" + cacheName);
4431 post.file_url = "/data/" + cacheName;
4432 }
4433 }
4434 else // Mark hidden img for fixing.
4435 post.thumb_class += " bbb-hidden-thumb";
4436 }
4437 }
4438
4439 function fixHiddenThumbs() {
4440 // Fix hidden thumbnails by fetching the info from a page.
4441 if (bbb.flags.hidden_xml)
4442 return;
4443
4444 var hiddenImgs = document.getElementsByClassName("bbb-hidden-thumb");
4445
4446 if (hiddenImgs[0]) {
4447 if (!bbb.cache.save_enabled) {
4448 window.addEventListener("beforeunload", updateThumbCache);
4449 bbb.cache.save_enabled = true;
4450 }
4451
4452 searchPages("hidden", hiddenImgs[0].getAttribute("data-id"));
4453 }
4454 }
4455
4456 function createThumbHTML(post, query) {
4457 // Create a thumbnail HTML string.
4458 return '<article class="post-preview' + post.thumb_class + '" id="post_' + post.id + '" data-id="' + post.id + '" data-has-sound="' + post.has_sound + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" data-rating="' + post.rating + '" data-width="' + post.image_width + '" data-height="' + post.image_height + '" data-flags="' + post.flags + '" data-parent-id="' + post.parent_id + '" data-has-children="' + post.has_children + '" data-score="' + post.score + '" data-fav-count="' + post.fav_count + '" data-approver-id="' + post.approver_id + '" data-pixiv-id="' + post.pixiv_id + '" data-md5="' + post.md5 + '" data-file-ext="' + post.file_ext + '" data-file-url="' + post.file_url + '" data-large-file-url="' + post.large_file_url + '" data-preview-file-url="' + post.preview_file_url + '"><a href="/posts/' + post.id + query + '"><img src="' + post.preview_file_url + '" alt="' + post.tag_string + '"></a></article>';
4459 }
4460
4461 function createThumb(post, query) {
4462 // Create a thumbnail element. (lazy method <_<)
4463 var childSpan = document.createElement("span");
4464 childSpan.innerHTML = createThumbHTML(post, query);
4465
4466 return childSpan.firstElementChild;
4467 }
4468
4469 function createThumbListing(posts, orderedIds) {
4470 // Create a listing of thumbnails.
4471 var thumbs = document.createDocumentFragment();
4472 var postHolder = {};
4473 var query = getThumbQuery();
4474 var thumb;
4475 var i, il; // Loop variables;
4476
4477 // Generate thumbnails.
4478 for (i = 0, il = posts.length; i < il; i++) {
4479 var post = formatInfo(posts[i]);
4480
4481 // Don't display loli/shota/toddlercon/deleted/banned if the user has opted so and skip to the next image.
4482 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) || (!show_banned && post.is_banned) || safebPostTest(post))
4483 continue;
4484
4485 // Check if the post is hidden.
4486 checkHiddenThumbs(post);
4487
4488 // eek, not so huge line.
4489 thumb = createThumb(post, query);
4490
4491 // Generate output.
4492 if (!orderedIds)
4493 thumbs.appendChild(thumb);
4494 else
4495 postHolder[post.id] = thumb;
4496 }
4497
4498 // Place thumbnails in the correct order for pools.
4499 if (orderedIds) {
4500 for (i = 0, il = orderedIds.length; i < il; i++) {
4501 thumb = postHolder[orderedIds[i]];
4502
4503 if (thumb)
4504 thumbs.appendChild(thumb);
4505 }
4506 }
4507
4508 return thumbs;
4509 }
4510
4511 function loadThumbCache() {
4512 // Initialize or load up the thumbnail cache.
4513 var thumbCache = localStorage.getItem("bbb_thumb_cache");
4514
4515 if (thumbCache !== null)
4516 bbb.cache.stored = JSON.parse(thumbCache);
4517 else {
4518 bbb.cache.stored = {history: [], names: {}};
4519 localStorage.bbbSetItem("bbb_thumb_cache", JSON.stringify(bbb.cache.stored));
4520 }
4521 }
4522
4523 function updateThumbCache() {
4524 // Add the current new thumbnail info to the saved thumbnail information.
4525 if (!bbb.cache.current.history[0] || !thumb_cache_limit)
4526 return;
4527
4528 loadThumbCache();
4529
4530 var bcc = bbb.cache.current;
4531 var bcs = bbb.cache.stored;
4532 var i, il; // Loop variables.
4533
4534 // Make sure we don't have duplicates in the new info.
4535 for (i = 0, il = bcc.history.length; i < il; i++) {
4536 if (bcs.names[bcc.history[i]]) {
4537 delete bcc.names[bcc.history[i]];
4538 bcc.history.splice(i, 1);
4539 il--;
4540 i--;
4541 }
4542 }
4543
4544 // Add the new thumbnail info in.
4545 for (i in bcc.names) {
4546 if (bcc.names.hasOwnProperty(i)) {
4547 bcs.names[i] = bcc.names[i];
4548 }
4549 }
4550
4551 bcs.history = bcs.history.concat(bcc.history);
4552
4553 // Prune the cache if it's larger than the user limit.
4554 if (bcs.history.length > thumb_cache_limit) {
4555 var removedIds = bcs.history.splice(0, bcs.history.length - thumb_cache_limit);
4556
4557 for (i = 0, il = removedIds.length; i < il; i++)
4558 delete bcs.names[removedIds[i]];
4559 }
4560
4561 localStorage.bbbSetItem("bbb_thumb_cache", JSON.stringify(bcs));
4562 bbb.cache.current = {history: [], names: {}};
4563 }
4564
4565 function adjustThumbCache() {
4566 // Prune the cache if it's larger than the user limit.
4567 loadThumbCache();
4568
4569 thumb_cache_limit = bbb.user.thumb_cache_limit;
4570
4571 var bcs = bbb.cache.stored;
4572
4573 if (bcs.history.length > thumb_cache_limit) {
4574 var removedIds = bcs.history.splice(0, bcs.history.length - thumb_cache_limit);
4575
4576 for (var i = 0, il = removedIds.length; i < il; i++)
4577 delete bcs.names[removedIds[i]];
4578 }
4579
4580 localStorage.bbbSetItem("bbb_thumb_cache", JSON.stringify(bcs));
4581 }
4582
4583 function getIdCache() {
4584 // Retrieve the cached list of post IDs used for the pool/favorite group thumbnails.
4585 var collId = /\/(?:pools|favorite_groups)\/(\d+)/.exec(location.href)[1];
4586 var idCache = sessionStorage.getItem("bbb_" + gLoc + "_cache_" + collId);
4587 var curTime = new Date().getTime();
4588 var cacheTime;
4589 var timeDiff;
4590
4591 if (idCache) {
4592 idCache = idCache.split(" ");
4593 cacheTime = idCache.shift();
4594 timeDiff = (curTime - cacheTime) / 1000; // Cache age in seconds.
4595 }
4596
4597 if (!idCache || (timeDiff && timeDiff > 600))
4598 return undefined;
4599 else
4600 return idCache.join(" ");
4601 }
4602
4603 function getThumbQuery() {
4604 // Return the thumbnail URL query value.
4605 var query = "";
4606
4607 if (gLoc === "search" || gLoc === "favorites") {
4608 query = getCurTags();
4609 query = (query ? "?tags=" + query : "");
4610 }
4611 else if (gLoc === "pool")
4612 query = "?pool_id=" + /\/pools\/(\d+)/.exec(location.pathname)[1];
4613 else if (gLoc === "favorite_group")
4614 query = "?favgroup_id=" + /\/favorite_groups\/(\d+)/.exec(location.pathname)[1];
4615
4616 return query;
4617 }
4618
4619 function getCurTags() {
4620 // Retrieve the current search tags for URL use.
4621 var tags;
4622
4623 if (gLoc === "search") {
4624 tags = getVar("tags") || "";
4625 }
4626 else if (gLoc === "favorites") {
4627 tags = document.getElementById("tags");
4628 tags = (tags ? tags.getAttribute("value").replace("fav:", "ordfav:").bbbSpaceClean() : ""); // Use getAttribute to avoid potential user changes to the input.
4629 }
4630
4631 return tags;
4632 }
4633
4634 function postDDL(target) {
4635 // Add direct downloads to thumbnails.
4636 if (!direct_downloads || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorites" && gLoc !== "favorite_group"))
4637 return;
4638
4639 var posts = getPosts(target);
4640
4641 for (var i = 0, il = posts.length; i < il; i++) {
4642 var post = posts[i];
4643 var postOrigUrl = post.getAttribute("data-file-url") || "";
4644 var postSampUrl = post.getAttribute("data-large-file-url") || "";
4645 var postUrl = (postSampUrl.indexOf(".webm") > -1 ? postSampUrl : postOrigUrl);
4646 var postId = post.getAttribute("data-id");
4647 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
4648
4649 if (!ddlLink) { // If the direct download doesn't already exist, create it.
4650 ddlLink = document.createElement("a");
4651 ddlLink.innerHTML = "Direct Download";
4652 ddlLink.href = postUrl || "DDL unavailable for post " + postId + ".jpg";
4653 ddlLink.id = "bbb-ddl-" + postId;
4654 ddlLink.className = "bbb-ddl";
4655 post.appendChild(ddlLink);
4656 }
4657 else if (ddlLink.href.indexOf("/data/") < 0) // Fix existing links for hidden thumbs.
4658 ddlLink.href = postUrl || "DDL unavailable for post " + postId + ".jpg";
4659 }
4660 }
4661
4662 function cleanLinks(target) {
4663 // Remove the query portion of thumbnail links.
4664 if (!clean_links)
4665 return;
4666
4667 var targetContainer;
4668 var links;
4669 var link;
4670 var linkParent;
4671
4672 if (target)
4673 targetContainer = target;
4674 else if (gLoc === "post")
4675 targetContainer = document.getElementById("content");
4676 else if (gLoc === "pool" || gLoc === "favorite_group") {
4677 targetContainer = document.getElementById("a-show");
4678 targetContainer = (targetContainer ? targetContainer.getElementsByTagName("section")[0] : undefined);
4679 }
4680 else if (gLoc === "search" || gLoc === "favorites")
4681 targetContainer = document.getElementById("posts");
4682 else if (gLoc === "intro")
4683 targetContainer = document.getElementById("a-intro");
4684
4685 if (targetContainer) {
4686 links = targetContainer.getElementsByTagName("a");
4687
4688 for (var i = 0, il = links.length; i < il; i++) {
4689 link = links[i];
4690 linkParent = link.parentNode;
4691
4692 if (linkParent.tagName === "ARTICLE" || linkParent.id.indexOf("nav-link-for-pool-") === 0)
4693 link.href = link.href.split("?", 1)[0];
4694 }
4695 }
4696 }
4697
4698 function danbModeMenu(target) {
4699 // Add mode menu functionality to newly created thumbnails.
4700 var modeSection = document.getElementById("mode-box");
4701
4702 if (!modeSection)
4703 return;
4704
4705 var links = (target || document).getElementsByClassName("bbb-thumb-link");
4706 var menuHandler = function(event) {
4707 if (event.button === 0)
4708 Danbooru.PostModeMenu.click(event);
4709 };
4710
4711 for (var i = 0, il = links.length; i < il; i++)
4712 links[i].addEventListener("click", menuHandler, false);
4713 }
4714
4715 function potentialHiddenPosts(mode, target) {
4716 // Check a normal thumbnail listing for possible hidden posts.
4717 var numPosts = getPosts(target).length;
4718 var noResults = noResultsPage(target);
4719 var limit = getLimit();
4720
4721 if (mode === "search" || mode === "notes" || mode === "favorites") {
4722 var numDesired;
4723 var numExpected;
4724
4725 numExpected = (limit !== undefined ? limit : thumbnail_count_default);
4726 numDesired = (allowUserLimit() ? thumbnail_count : numExpected);
4727
4728 if (!noResults && (numPosts !== numDesired || numPosts < numExpected))
4729 return true;
4730 }
4731 else if (mode === "popular" || mode === "pool" || mode === "favorite_group" || mode === "popular_view") {
4732 if (!noResults && numPosts !== limit)
4733 return true;
4734 }
4735 else if (mode === "comments") {
4736 if (numPosts !== limit)
4737 return true;
4738 }
4739
4740 return false;
4741 }
4742
4743 /* Endless Page functions */
4744 function endlessToggle(event) {
4745 // Toggle endless pages on and off.
4746 if (endless_default === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "favorite_group"))
4747 return;
4748
4749 // Change the default for the duration of the session if necessary.
4750 if (endless_session_toggle) {
4751 var onValue = (bbb.user.endless_default !== "off" ? bbb.user.endless_default : "on");
4752 var newDefault = (bbb.endless.enabled ? "off" : onValue);
4753
4754 endless_default = newDefault;
4755 sessionStorage.bbbSetItem("bbb_endless_default", newDefault);
4756 }
4757
4758 if (bbb.endless.enabled) {
4759 endlessDisable();
4760
4761 if (event && event.type !== "click")
4762 bbbNotice("Endless pages disabled.", 2);
4763 }
4764 else {
4765 endlessEnable();
4766
4767 if (event && event.type !== "click")
4768 bbbNotice("Endless pages enabled.", 2);
4769 }
4770 }
4771
4772 function endlessEnable() {
4773 // Turn on endless pages.
4774 if (endless_default === "disabled" || noXML())
4775 return;
4776
4777 bbb.endless.enabled = true;
4778 bbb.el.endlessEnableDiv.style.display = "none";
4779 bbb.el.endlessLoadDiv.style.display = "inline-block";
4780 bbb.el.endlessLink.style.fontWeight = "bold";
4781
4782 // Check on the next page status.
4783 endlessCheck();
4784
4785 // Add the listeners for detecting the amount of scroll left.
4786 window.addEventListener("scroll", endlessCheck, false);
4787 window.addEventListener("resize", endlessCheck, false);
4788 document.addEventListener("keyup", endlessCheck, false);
4789 document.addEventListener("click", endlessCheck, false);
4790 }
4791
4792 function endlessDisable() {
4793 // Turn off endless pages.
4794 bbb.endless.enabled = false;
4795 bbb.endless.append_page = false;
4796 bbb.el.endlessEnableDiv.style.display = "inline-block";
4797 bbb.el.endlessLoadDiv.style.display = "none";
4798 bbb.el.endlessLink.style.fontWeight = "normal";
4799
4800 // Remove the listeners for detecting the amount of scroll left.
4801 window.removeEventListener("scroll", endlessCheck, false);
4802 window.removeEventListener("resize", endlessCheck, false);
4803 document.removeEventListener("keyup", endlessCheck, false);
4804 document.removeEventListener("click", endlessCheck, false);
4805 }
4806
4807 function endlessInit() {
4808 // Set up and start endless pages.
4809 removeInheritedStorage("bbb_endless_default");
4810
4811 if (endless_default === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "favorite_group"))
4812 return;
4813
4814 // Add the endless link to the menu.
4815 var menu = document.getElementById("top");
4816 menu = (menu ? menu.getElementsByTagName("menu")[1] : undefined);
4817
4818 if (menu) {
4819 var menuItems = menu.getElementsByTagName("li");
4820 var numMenuItems = menu.getElementsByTagName("li").length;
4821 var listingItemSibling = menuItems[1];
4822
4823 for (var i = 0; i < numMenuItems; i++) {
4824 var menuLink = menuItems[i];
4825 var nextLink = menuItems[i + 1];
4826
4827 if (menuLink.textContent.indexOf("Listing") > -1) {
4828 if (nextLink)
4829 listingItemSibling = nextLink;
4830 else
4831 listingItemSibling = undefined;
4832
4833 break;
4834 }
4835 }
4836
4837 var link = bbb.el.endlessLink = document.createElement("a");
4838 link.href = "#";
4839 link.innerHTML = "Endless";
4840 link.addEventListener("click", function(event) {
4841 endlessToggle();
4842 event.preventDefault();
4843 }, false);
4844
4845 var item = document.createElement("li");
4846 item.style.textAlign = "center";
4847 item.style.display = "inline-block";
4848 item.appendChild(link);
4849
4850 if (listingItemSibling)
4851 menu.insertBefore(item, listingItemSibling);
4852 else
4853 menu.appendChild(item);
4854
4855 link.style.fontWeight = "bold";
4856 item.style.width = item.clientWidth + "px";
4857 link.style.fontWeight = "normal";
4858 }
4859
4860 var paginator = getPaginator();
4861
4862 if (paginator) {
4863 var paginatorParent = paginator.parentNode;
4864
4865 // Set up the load more button.
4866 var buttonDiv = document.createElement("div");
4867 buttonDiv.id = "bbb-endless-button-div";
4868
4869 var loadButtonDiv = bbb.el.endlessLoadDiv = document.createElement("div");
4870 loadButtonDiv.id = "bbb-endless-load-div";
4871 buttonDiv.appendChild(loadButtonDiv);
4872
4873 var loadButton = bbb.el.endlessLoadButton = document.createElement("a");
4874 loadButton.innerHTML = "Load More";
4875 loadButton.href = "#";
4876 loadButton.id = "bbb-endless-load-button";
4877 loadButton.style.display = "none";
4878 loadButton.addEventListener("click", function(event) {
4879 loadButton.style.display = "none";
4880 loadButton.blur();
4881 bbb.endless.paused = false;
4882 bbb.endless.append_page = true;
4883 endlessCheck();
4884 event.preventDefault();
4885 }, false);
4886 loadButtonDiv.appendChild(loadButton);
4887
4888 // Set up the enable button.
4889 var enableButtonDiv = bbb.el.endlessEnableDiv = document.createElement("div");
4890 enableButtonDiv.id = "bbb-endless-enable-div";
4891 buttonDiv.appendChild(enableButtonDiv);
4892
4893 var enableButton = document.createElement("a");
4894 enableButton.innerHTML = "Endless";
4895 enableButton.href = "#";
4896 enableButton.id = "bbb-endless-enable-button";
4897 enableButton.addEventListener("click", function(event) {
4898 enableButton.blur();
4899 endlessToggle();
4900 event.preventDefault();
4901 }, false);
4902 enableButtonDiv.appendChild(enableButton);
4903
4904 paginatorParent.insertBefore(buttonDiv, paginator);
4905 }
4906
4907 // Check the session default or original default value to see if endless pages should be enabled.
4908 var sessionDefault = sessionStorage.getItem("bbb_endless_default");
4909
4910 if (endless_session_toggle && sessionDefault)
4911 endless_default = sessionDefault;
4912
4913 if (endless_default !== "off")
4914 endlessEnable();
4915 else
4916 endlessDisable();
4917 }
4918
4919 function endlessObjectInit() {
4920 // Initialize the values for the first XML request. Runs separately from endlessInit since it requires the initial page being finalized.
4921 var posts = getPosts();
4922 var numPosts = posts.length;
4923
4924 // Prep the first paginator.
4925 bbb.endless.last_paginator = getPaginator();
4926
4927 // If we're already on the last page, don't continue.
4928 if (endlessLastPage())
4929 return;
4930
4931 // Note the posts that already exist.
4932 if (endless_remove_dup) {
4933 for (var i = 0; i < numPosts; i++) {
4934 var post = posts[i];
4935
4936 bbb.endless.posts[post.id] = post;
4937 }
4938 }
4939
4940 // Create a special "page" for filling out the first page.
4941 if (endless_fill) {
4942 var limit = getLimit() || thumbnail_count_default;
4943
4944 if (numPosts < limit) {
4945 var newPageObject = {
4946 page: document.createElement("span"),
4947 page_num: [(getVar("page") || "1")],
4948 paginator: bbb.endless.last_paginator,
4949 ready: false
4950 };
4951
4952 bbb.endless.fill_first_page = true;
4953 bbb.endless.pages.push(newPageObject);
4954 }
4955 }
4956 }
4957
4958 function endlessCheck() {
4959 // Check whether the current document is ready for a page to be appended.
4960 if (!bbb.endless.enabled)
4961 return;
4962
4963 // Check whether endless pages needs to be paused.
4964 endlessPauseCheck();
4965
4966 // Stop if the check is delayed.
4967 if (bbb.timers.endlessDelay)
4968 return;
4969
4970 // Check whether a user is looking at the "posts tab" and not the "wiki tab" in the main search listing.
4971 var postsDiv = (gLoc === "search" ? document.getElementById("posts") : undefined);
4972 var postsVisible = (!postsDiv || postsDiv.style.display !== "none");
4973
4974 if (bbb.flags.thumbs_xml || bbb.flags.paginator_xml || !postsVisible) // Delay the check until the page is completely ready.
4975 endlessDelay(100);
4976 else {
4977 if (!bbb.endless.last_paginator)
4978 endlessObjectInit();
4979
4980 if (bbb.endless.append_page)
4981 endlessQueueCheck();
4982 else { // Check the amount of space left to scroll and attempt to add a page if we're far enough down.
4983 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
4984 var viewHeight = document.documentElement.clientHeight;
4985 var docHeight = document.documentElement.offsetHeight;
4986
4987 if (docHeight <= viewHeight + scrolled + endless_scroll_limit)
4988 bbb.endless.append_page = true;
4989
4990 endlessQueueCheck();
4991 }
4992 }
4993 }
4994
4995 function endlessQueueCheck() {
4996 // Check the page queue and append or request a page.
4997 if (!bbb.endless.enabled || bbb.endless.paused)
4998 return;
4999
5000 if (bbb.endless.append_page || bbb.endless.fill_first_page) {
5001 if (endlessPageReady())
5002 endlessAppendPage();
5003 else
5004 endlessRequestPage();
5005 }
5006 else if (endless_preload && !endlessPageReady())
5007 endlessRequestPage();
5008 }
5009
5010 function endlessRequestPage() {
5011 // Start an XML request for a new page.
5012 if (bbb.flags.endless_xml || endlessLastPage()) // Retrieve pages one at a time for as long as they exist.
5013 return;
5014
5015 searchPages("endless");
5016 }
5017
5018 function endlessQueuePage(newPage) {
5019 // Take some thumbnails from a page and work them into the queue.
5020 var limit = getLimit() || thumbnail_count_default;
5021 var pageNum = getVar("page", endlessNexURL());
5022 var paginator = bbb.endless.last_paginator = bbb.endless.new_paginator;
5023 var badPaginator = (paginator.textContent.indexOf("Go back") > -1); // Sometimes the paginator sends us to a page with no results.
5024 var lastPage = endlessLastPage() || badPaginator;
5025 var posts = getPosts(newPage);
5026
5027 bbb.endless.new_paginator = undefined;
5028
5029 // Remove duplicates.
5030 if (endless_remove_dup) {
5031 for (var i = 0; i < posts.length; i++) {
5032 var post = posts[i];
5033 var postId = post.id;
5034
5035 if (bbb.endless.posts[postId]) {
5036 newPage.removeChild(post);
5037 i--;
5038 }
5039 else
5040 bbb.endless.posts[postId] = post;
5041 }
5042 }
5043
5044 // Fill up existing page objects with thumbnails.
5045 var lastPageObject = bbb.endless.pages[bbb.endless.pages.length - 1];
5046
5047 if (endless_fill && lastPageObject && !lastPageObject.ready) {
5048 if (badPaginator) // Paginator isn't accurate. Ignore this page's number and paginator.
5049 lastPageObject.ready = true;
5050 else {
5051 var lastQueuePage = lastPageObject.page;
5052 var lastQueuePosts = getPosts(lastQueuePage);
5053 var fillLimit = (bbb.endless.fill_first_page ? limit - getPosts().length : limit);
5054
5055 while (!lastPageObject.ready && posts[0]) {
5056 lastQueuePage.appendChild(posts[0]);
5057
5058 if (lastQueuePosts.length === fillLimit)
5059 lastPageObject.ready = true;
5060 }
5061
5062 // If there are no more posts and pages, mark the last page as ready.
5063 if (!lastPageObject.ready && !posts[0] && lastPage)
5064 lastPageObject.ready = true;
5065
5066 // Make sure the displayed paginator is always the one from the last retrieved page to have all of it's thumbnails used so the user doesn't click to the next page and skip queued thumbnails that haven't been displayed yet.
5067 if (!posts[0])
5068 lastPageObject.paginator = paginator;
5069
5070 lastPageObject.page_num.push(pageNum);
5071 }
5072 }
5073
5074 // Queue up a new page object.
5075 var numNewPosts = posts.length;
5076
5077 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.
5078 var newPageObject = {
5079 page: newPage,
5080 page_num: [pageNum],
5081 paginator: paginator,
5082 ready: (!endless_fill || numNewPosts === limit || lastPage ? true : false)
5083 };
5084
5085 bbb.endless.pages.push(newPageObject);
5086
5087 if (bbb.endless.no_thumb_count < 10)
5088 bbb.endless.no_thumb_count = 0;
5089 }
5090 else if (!badPaginator)
5091 bbb.endless.no_thumb_count++;
5092
5093 // Get rid of the load more button for special circumstances where the paginator isn't accurate.
5094 if (lastPage && !bbb.endless.pages[0])
5095 bbb.el.endlessLoadButton.style.display = "none";
5096
5097 // Warn the user if this is a listing full of hidden posts.
5098 if (bbb.endless.no_thumb_count === 10) {
5099 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);
5100 endlessPause();
5101 }
5102 else
5103 endlessQueueCheck();
5104 }
5105
5106 function endlessAppendPage() {
5107 // Prep the first queued page object and add it to the document.
5108 var firstPageObject = bbb.endless.pages.shift();
5109 var page = firstPageObject.page;
5110 var thumbContainer = getThumbContainer(gLoc);
5111 var before = getThumbSibling(gLoc);
5112
5113 // Prepare thumbnails.
5114 prepThumbnails(page);
5115
5116 // Page separators.
5117 var pageNum = firstPageObject.page_num;
5118 var numPageNum = pageNum.length;
5119 var firstNum = pageNum[0];
5120 var lastNum = (numPageNum > 1 ? pageNum[numPageNum - 1] : undefined);
5121
5122 if (endless_separator === "divider") {
5123 var divider = document.createElement("div");
5124 divider.className = "bbb-endless-divider";
5125
5126 var dividerLink = document.createElement("div");
5127 dividerLink.className = "bbb-endless-divider-link";
5128 divider.appendChild(dividerLink);
5129
5130 dividerLink.innerHTML = '<a href="' + updateURLQuery(location.href, {page: firstNum}) + '">Page ' + firstNum + '</a>' + (lastNum ? ' ~ <a href="' + updateURLQuery(location.href, {page: lastNum}) + '">Page ' + lastNum + '</a>' : '');
5131
5132 if (!bbb.endless.fill_first_page)
5133 page.insertBefore(divider, page.firstElementChild);
5134 else if (lastNum) // Only add the divider for filling the first page when there are actual posts added.
5135 thumbContainer.insertBefore(divider, (getPosts()[0] || before));
5136 }
5137 else if (endless_separator === "marker") {
5138 var markerContainer = document.createElement("article");
5139 markerContainer.className = "bbb-endless-marker-article";
5140
5141 var marker = document.createElement("div");
5142 marker.className = "bbb-endless-marker";
5143 markerContainer.appendChild(marker);
5144
5145 var markerLink = document.createElement("div");
5146 markerLink.className = "bbb-endless-marker-link";
5147 marker.appendChild(markerLink);
5148
5149 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>' : '');
5150
5151 if (!bbb.endless.fill_first_page)
5152 page.insertBefore(markerContainer, page.firstElementChild);
5153 else if (lastNum) // Only add the marker for filling the first page when there are actual posts added.
5154 thumbContainer.insertBefore(markerContainer, (getPosts()[0] || before));
5155 }
5156
5157 // Add the new page.
5158 if (!before)
5159 thumbContainer.appendChild(page);
5160 else
5161 thumbContainer.insertBefore(page, before);
5162
5163 // Replace the paginator.
5164 replacePaginator(firstPageObject.paginator);
5165
5166 // Fix hidden thumbnails.
5167 fixHiddenThumbs();
5168 bbbStatus("hidden", "new"); // Update status message with new amount.
5169
5170 if (!bbb.endless.fill_first_page)
5171 bbb.endless.append_page = false;
5172 else {
5173 bbb.endless.fill_first_page = false;
5174 endlessQueueCheck();
5175 }
5176
5177 if (quick_search.indexOf("remove") > -1 && bbb.quick_search !== "")
5178 endlessDelay(1100);
5179
5180 endlessCheck();
5181 }
5182
5183 function endlessNexURL() {
5184 // Get the URL of the next new page.
5185 return getPaginatorNextURL(bbb.endless.last_paginator);
5186 }
5187
5188 function endlessPageReady() {
5189 // Check if the first queued page object is ready to be appended.
5190 var firstPageObject = bbb.endless.pages[0];
5191
5192 return (firstPageObject && firstPageObject.ready);
5193 }
5194
5195 function endlessLastPage() {
5196 // Check if there isn't a next page.
5197 return (!endlessNexURL() || noResultsPage());
5198 }
5199
5200 function endlessPauseCheck() {
5201 // Check if loading needs to be paused due to the interval or default.
5202 if (bbb.endless.append_page)
5203 return;
5204
5205 var numPages = document.getElementsByClassName("bbb-endless-page").length + 1;
5206
5207 if (numPages % endless_pause_interval === 0 || (endless_default === "paused" && numPages === 1))
5208 endlessPause();
5209 }
5210
5211 function endlessPause() {
5212 // Pause endless pages so that it can't add any more pages.
5213 if (bbb.endless.paused || (endlessLastPage() && !bbb.endless.pages[0]))
5214 return;
5215
5216 bbb.endless.paused = true;
5217 bbb.endless.append_page = false;
5218 bbb.el.endlessLoadButton.style.display = "inline-block";
5219 }
5220
5221 function endlessDelay(ms) {
5222 // Delay endless pages for the provided number of milliseconds.
5223 bbb.timers.endlessDelay = window.setTimeout( function() {
5224 bbb.timers.endlessDelay = 0;
5225
5226 endlessCheck();
5227 }, ms);
5228 }
5229
5230 /* Blacklist Functions */
5231 function blacklistInit() {
5232 // Reset the blacklist with the account settings when logged in or script settings when logged out/using the override.
5233 var blacklistTags = accountSettingCheck("script_blacklisted_tags");
5234 var blacklistBox = document.getElementById("blacklist-box");
5235 var blacklistList = document.getElementById("blacklist-list");
5236 var enableLink = document.getElementById("re-enable-all-blacklists");
5237 var disableLink = document.getElementById("disable-all-blacklists");
5238 var blacklistedPosts = document.getElementsByClassName("blacklisted");
5239 var i, il; // Loop variables.
5240
5241 // Reset the list or create it as needed.
5242 if (blacklistBox && blacklistList) {
5243 blacklistBox.style.display = "none";
5244
5245 var childIndex = 0;
5246
5247 while (blacklistList.children[childIndex]) {
5248 var child = blacklistList.children[childIndex];
5249
5250 if (child.getElementsByTagName("a")[0] && child !== enableLink && child !== disableLink)
5251 blacklistList.removeChild(child);
5252 else
5253 childIndex++;
5254 }
5255 }
5256 else if (blacklist_add_bars) {
5257 var target;
5258 var before;
5259
5260 if (gLoc === "comment_search") {
5261 target = document.getElementById("a-index");
5262
5263 if (target)
5264 before = target.getElementsByClassName("comments-for-post")[0];
5265 }
5266 else if (gLoc === "comment") {
5267 target = document.getElementById("a-show");
5268
5269 if (target)
5270 before = target.getElementsByClassName("comments-for-post")[0];
5271 }
5272
5273 if (target && before && before.parentNode === target) {
5274 blacklistBox = document.createElement("div");
5275 blacklistBox.id = "blacklist-box";
5276 blacklistBox.className = "bbb-blacklist-box";
5277 blacklistBox.style.display = "none";
5278 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>';
5279
5280 blacklistList = getId("blacklist-list", blacklistBox);
5281 enableLink = getId("re-enable-all-blacklists", blacklistBox);
5282 disableLink = getId("disable-all-blacklists", blacklistBox);
5283
5284 target.insertBefore(blacklistBox, before);
5285 }
5286 }
5287
5288 // Reset any blacklist info.
5289 if (bbb.blacklist.entries[0]) {
5290 delete bbb.blacklist;
5291 bbb.blacklist = {entries: [], match_list: {}, smart_view_target: undefined};
5292 }
5293
5294 // Reset any blacklisted thumbnails.
5295 while (blacklistedPosts[0])
5296 blacklistedPosts[0].bbbRemoveClass("blacklisted blacklisted-active");
5297
5298 // Check if there actually are any tags.
5299 if (!blacklistTags || !/[^\s,]/.test(blacklistTags))
5300 return;
5301
5302 // Preserve commas within nested/grouped tags.
5303 var groupsObject = replaceSearchGroups(blacklistTags);
5304 var groups = groupsObject.groups;
5305
5306 blacklistTags = groupsObject.search.replace(/,/g, "%,%");
5307 blacklistTags = restoreSearchGroups(blacklistTags, groups);
5308 blacklistTags = blacklistTags.split("%,%");
5309
5310 // Create the blacklist section.
5311 var cookies = getCookie();
5312 var blacklistDisabled = (cookies.dab === "1" && blacklistBox);
5313
5314 for (i = 0, il = blacklistTags.length; i < il; i++) {
5315 var blacklistTag = blacklistTags[i].bbbSpaceClean();
5316 var blacklistSearch = createSearch(blacklistTag);
5317
5318 if (blacklistSearch[0]) {
5319 var entryHash = blacklistTag.bbbHash();
5320 var entryDisabled = (blacklistDisabled || (blacklist_session_toggle && cookies["b" + entryHash] === "1") ? true : false);
5321 var newEntry = {active: !entryDisabled, tags:blacklistTag, search:blacklistSearch, matches: [], index: i, hash: entryHash};
5322
5323 bbb.blacklist.entries.push(newEntry);
5324
5325 if (blacklistList) {
5326 var blacklistItem = document.createElement("li");
5327 blacklistItem.title = blacklistTag;
5328 blacklistItem.className = "bbb-blacklist-item-" + i;
5329 blacklistItem.style.display = "none";
5330
5331 var blacklistLink = document.createElement("a");
5332 blacklistLink.innerHTML = (blacklistTag.length < 19 ? blacklistTag + " " : blacklistTag.substring(0, 18).bbbSpaceClean() + "... ");
5333 blacklistLink.className = "bbb-blacklist-entry-" + i + (entryDisabled ? " blacklisted-active" : "");
5334 blacklistLink.setAttribute("data-bbb-blacklist-entry", i);
5335 blacklistLink.addEventListener("click", blacklistEntryLinkToggle, false);
5336 blacklistItem.appendChild(blacklistLink);
5337
5338 var blacklistCount = document.createElement("span");
5339 blacklistCount.className = "count";
5340 blacklistCount.innerHTML = "0";
5341 blacklistItem.appendChild(blacklistCount);
5342
5343 blacklistList.appendChild(blacklistItem);
5344 }
5345 }
5346 }
5347
5348 // Replace the disable/enable all blacklist links with our own.
5349 if (enableLink && disableLink) {
5350 var newEnableLink = bbb.el.blacklistEnableLink = enableLink.cloneNode(true);
5351 var newDisableLink = bbb.el.blacklistDisableLink = disableLink.cloneNode(true);
5352
5353 newEnableLink.addEventListener("click", blacklistLinkToggle, false);
5354 newDisableLink.addEventListener("click", blacklistLinkToggle, false);
5355
5356 if (blacklistDisabled) {
5357 newEnableLink.style.display = "inline";
5358 newDisableLink.style.display = "none";
5359 }
5360 else {
5361 newEnableLink.style.display = "none";
5362 newDisableLink.style.display = "inline";
5363 }
5364
5365 enableLink.parentNode.replaceChild(newEnableLink, enableLink);
5366 disableLink.parentNode.replaceChild(newDisableLink, disableLink);
5367 }
5368
5369 // Test all posts on the page for a match and set up the initial blacklist.
5370 blacklistUpdate();
5371 }
5372
5373 function blacklistLinkToggle(event) {
5374 // Event listener function for permanently toggling the entire blacklist.
5375 if (event.button !== 0)
5376 return;
5377
5378 var blacklistDisabled = (getCookie().dab === "1");
5379 var entries = bbb.blacklist.entries;
5380
5381 if (blacklistDisabled) {
5382 bbb.el.blacklistEnableLink.style.display = "none";
5383 bbb.el.blacklistDisableLink.style.display = "inline";
5384 createCookie("dab", 0, 365);
5385 }
5386 else {
5387 bbb.el.blacklistEnableLink.style.display = "inline";
5388 bbb.el.blacklistDisableLink.style.display = "none";
5389 createCookie("dab", 1, 365);
5390 }
5391
5392 for (var i = 0, il = entries.length; i < il; i++) {
5393 var entry = entries[i];
5394
5395 if (blacklistDisabled) {
5396 if (!entry.active)
5397 blacklistEntryToggle(i);
5398 }
5399 else {
5400 if (entry.active)
5401 blacklistEntryToggle(i);
5402
5403 if (blacklist_session_toggle)
5404 createCookie("b" + entry.hash, 0, -1);
5405 }
5406 }
5407
5408 event.preventDefault();
5409 }
5410
5411 function blacklistEntryLinkToggle(event) {
5412 // Event listener function for blacklist entry toggle links.
5413 if (event.button !== 0)
5414 return;
5415
5416 var entryNumber = Number(event.target.getAttribute("data-bbb-blacklist-entry"));
5417
5418 blacklistEntryToggle(entryNumber);
5419
5420 event.preventDefault();
5421 }
5422
5423 function blacklistEntryToggle(entryIndex) {
5424 // Toggle a blacklist entry and adjust all of its related elements.
5425 var entry = bbb.blacklist.entries[entryIndex];
5426 var matches = entry.matches;
5427 var links = document.getElementsByClassName("bbb-blacklist-entry-" + entryIndex);
5428 var blacklistDisabled = (getCookie().dab === "1");
5429 var id;
5430 var els;
5431 var matchList;
5432 var i, il, j, jl; // Loop variables.
5433
5434 if (entry.active) {
5435 entry.active = false;
5436
5437 if (blacklist_session_toggle && !blacklistDisabled)
5438 createCookie("b" + entry.hash, 1);
5439
5440 for (i = 0, il = links.length; i < il; i++)
5441 links[i].bbbAddClass("blacklisted-active");
5442
5443 for (i = 0, il = matches.length; i < il; i++) {
5444 id = matches[i];
5445 matchList = bbb.blacklist.match_list[id];
5446
5447 matchList.count--;
5448
5449 if (!matchList.count && matchList.override !== false) {
5450 if (id === "image-container")
5451 document.getElementById("image-container").bbbRemoveClass("blacklisted-active");
5452 else {
5453 els = document.getElementsByClassName(id);
5454
5455 for (j = 0, jl = els.length; j < jl; j++)
5456 els[j].bbbRemoveClass("blacklisted-active");
5457 }
5458 }
5459 }
5460 }
5461 else {
5462 entry.active = true;
5463
5464 if (blacklist_session_toggle)
5465 createCookie("b" + entry.hash, 0, -1);
5466
5467 for (i = 0, il = links.length; i < il; i++)
5468 links[i].bbbRemoveClass("blacklisted-active");
5469
5470 for (i = 0, il = matches.length; i < il; i++) {
5471 id = matches[i];
5472 matchList = bbb.blacklist.match_list[id];
5473
5474 matchList.count++;
5475
5476 if (matchList.override !== true) {
5477 if (id === "image-container")
5478 document.getElementById("image-container").bbbAddClass("blacklisted-active");
5479 else {
5480 els = document.getElementsByClassName(id);
5481
5482 for (j = 0, jl = els.length; j < jl; j++)
5483 els[j].bbbAddClass("blacklisted-active");
5484 }
5485 }
5486 }
5487 }
5488 }
5489
5490 function blacklistUpdate(target) {
5491 // Update the blacklists without resetting everything.
5492 if (!bbb.blacklist.entries[0])
5493 return;
5494
5495 // Retrieve the necessary elements from the target element or current document.
5496 var blacklistBox = getId("blacklist-box", target) || document.getElementById("blacklist-box");
5497 var blacklistList = getId("blacklist-list", target) || document.getElementById("blacklist-list");
5498 var imgContainer = getId("image-container", target);
5499 var posts = getPosts(target);
5500
5501 var i, il; // Loop variables.
5502
5503 // Test the image for a match when viewing a post.
5504 if (imgContainer) {
5505 var imgId = imgContainer.getAttribute("data-id");
5506
5507 if (!blacklistSmartViewCheck(imgId))
5508 blacklistTest(imgContainer);
5509 }
5510
5511 // Search the posts for matches.
5512 for (i = 0, il = posts.length; i < il; i++)
5513 blacklistTest(posts[i]);
5514
5515 // Update the blacklist sidebar section match counts and display any blacklist items that have a match.
5516 if (blacklistBox && blacklistList) {
5517 for (i = 0, il = bbb.blacklist.entries.length; i < il; i++) {
5518 var entryLength = bbb.blacklist.entries[i].matches.length;
5519 var item = blacklistList.getElementsByClassName("bbb-blacklist-item-" + i)[0];
5520
5521 if (entryLength) {
5522 blacklistBox.style.display = "block";
5523 item.style.display = "";
5524 item.getElementsByClassName("count")[0].innerHTML = entryLength;
5525 }
5526 }
5527 }
5528 }
5529
5530 function blacklistTest(el) {
5531 // Test a post/image for a blacklist match and use its ID to store its info.
5532 var id = el.id;
5533 var matchList = bbb.blacklist.match_list[id];
5534
5535 if (typeof(matchList) === "undefined") { // Post hasn't been tested yet.
5536 matchList = bbb.blacklist.match_list[id] = {count: undefined, matches: [], override: undefined};
5537
5538 for (var i = 0, il = bbb.blacklist.entries.length; i < il; i++) {
5539 var entry = bbb.blacklist.entries[i];
5540
5541 if (thumbSearchMatch(el, entry.search)) {
5542 el.bbbAddClass("blacklisted");
5543
5544 if (entry.active) {
5545 el.bbbAddClass("blacklisted-active");
5546 matchList.count = ++matchList.count || 1;
5547 }
5548 else
5549 matchList.count = matchList.count || 0;
5550
5551 matchList.matches.push(entry);
5552 entry.matches.push(id);
5553 }
5554 }
5555
5556 if (matchList.count === undefined) // No match.
5557 matchList.count = false;
5558 else if (el.id !== "image-container") { // Match found so prepare the thumbnail.
5559 if (blacklist_thumb_controls)
5560 blacklistPostControl(el, matchList);
5561
5562 if (blacklist_smart_view)
5563 blacklistSmartView(el);
5564 }
5565
5566 }
5567 else if (matchList.count !== false && !el.bbbHasClass("blacklisted")) { // Post is already tested, but needs to be set up again.
5568 if (matchList.count > 0 && matchList.override !== true)
5569 el.bbbAddClass("blacklisted blacklisted-active");
5570 else
5571 el.bbbAddClass("blacklisted");
5572
5573 if (el.id !== "image-container") {
5574 if (blacklist_thumb_controls)
5575 blacklistPostControl(el, matchList);
5576
5577 if (blacklist_smart_view)
5578 blacklistSmartView(el);
5579 }
5580 }
5581 }
5582
5583 function blacklistPostControl(el, matchList) {
5584 // Add the blacklist post controls to a thumbnail.
5585 var target = el.getElementsByClassName("preview")[0] || el;
5586 var id = el.id;
5587 var tip = bbb.el.blacklistTip;
5588
5589 if (!tip) { // Create the tip if it doesn't exist.
5590 tip = bbb.el.blacklistTip = document.createElement("div");
5591 tip.id = "bbb-blacklist-tip";
5592 document.body.appendChild(tip);
5593 }
5594
5595 if (target) {
5596 // Set up the tip events listeners for hiding and displaying it.
5597 target.addEventListener("click", function(event) {
5598 if (event.button !== 0 || event.ctrlKey || event.shiftKey || event.altKey)
5599 return;
5600
5601 var target = event.target;
5602 var blacklistTip = bbb.el.blacklistTip;
5603 var i, il; // Loop variables.
5604
5605 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.
5606 return;
5607
5608 if (blacklistTip.style.display !== "block") {
5609 var matchEntries = matchList.matches;
5610 var tipContent = document.createDocumentFragment();
5611
5612 var header = document.createElement("b");
5613 header.innerHTML = "Blacklist Matches";
5614 tipContent.appendChild(header);
5615
5616 var list = document.createElement("ul");
5617 tipContent.appendChild(list);
5618
5619 for (i = 0, il = matchEntries.length; i < il; i++) {
5620 var matchEntry = matchEntries[i];
5621 var entryIndex = matchEntry.index;
5622 var blacklistTag = matchEntry.tags;
5623
5624 var blacklistItem = document.createElement("li");
5625 blacklistItem.title = blacklistTag;
5626
5627 var blacklistLink = document.createElement("a");
5628 blacklistLink.href = "#";
5629 blacklistLink.className = "bbb-blacklist-entry-" + entryIndex + (matchEntry.active ? "" : " blacklisted-active");
5630 blacklistLink.setAttribute("data-bbb-blacklist-entry", entryIndex);
5631 blacklistLink.innerHTML = (blacklistTag.length < 51 ? blacklistTag + " " : blacklistTag.substring(0, 50).bbbSpaceClean() + "...");
5632 blacklistLink.addEventListener("click", blacklistEntryLinkToggle, false);
5633 blacklistItem.appendChild(blacklistLink);
5634
5635 list.appendChild(blacklistItem);
5636 }
5637
5638 var viewLinkDiv = document.createElement("div");
5639 viewLinkDiv.style.marginTop = "1em";
5640 viewLinkDiv.style.textAlign = "center";
5641 viewLinkDiv.innerHTML = '<a class="bbb-post-link" id="bbb-blacklist-view-link" href="/posts/' + id.match(/\d+/)[0] + '">View post</a>';
5642 tipContent.appendChild(viewLinkDiv);
5643
5644 if (blacklist_smart_view) {
5645 var viewLink = getId("bbb-blacklist-view-link", viewLinkDiv);
5646
5647 if (viewLink) {
5648 viewLink.addEventListener("click", function(event) {
5649 if (event.button === 0)
5650 blacklistSmartViewUpdate(el);
5651 }, false);
5652 }
5653 }
5654
5655 blacklistShowTip(event, tipContent);
5656 }
5657 else {
5658 var els = document.getElementsByClassName(id);
5659
5660 for (i = 0, il = els.length; i < il; i++)
5661 els[i].bbbRemoveClass("blacklisted-active");
5662
5663 blacklistHideTip();
5664 bbb.blacklist.match_list[id].override = true;
5665 }
5666
5667 event.preventDefault();
5668 event.stopPropagation();
5669 }, true);
5670 target.addEventListener("mouseleave", function() { bbb.timers.blacklistTip = window.setTimeout(blacklistHideTip, 100); }, false);
5671 tip.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.blacklistTip); }, false);
5672 tip.addEventListener("mouseleave", blacklistHideTip, false);
5673
5674 // Add the hide button.
5675 var hide = document.createElement("span");
5676 hide.className = "bbb-close-circle";
5677 hide.addEventListener("click", function(event) {
5678 if (event.button !== 0)
5679 return;
5680
5681 var els = document.getElementsByClassName(id);
5682
5683 for (var i = 0, il = els.length; i < il; i++)
5684 els[i].bbbAddClass("blacklisted-active");
5685
5686 bbb.blacklist.match_list[id].override = false;
5687 }, false);
5688 target.appendChild(hide);
5689 }
5690 }
5691
5692 function blacklistShowTip(event, content) {
5693 // Display the blacklist control tip.
5694 var x = event.pageX;
5695 var y = event.pageY;
5696 var tip = bbb.el.blacklistTip;
5697
5698 formatTip(event, tip, content, x, y);
5699 }
5700
5701 function blacklistHideTip() {
5702 // Reset the blacklist control tip to hidden.
5703 var tip = bbb.el.blacklistTip;
5704
5705 if (tip)
5706 tip.removeAttribute("style");
5707 }
5708
5709 function blacklistSmartView(el) {
5710 // Set up the smart view event listeners.
5711 var img = el.getElementsByTagName("img")[0];
5712 var link = (img ? img.parentNode : undefined);
5713
5714 if (!link)
5715 return;
5716
5717 // Normal left click support.
5718 link.addEventListener("click", function(event) {
5719 if (event.button === 0)
5720 blacklistSmartViewUpdate(el);
5721 }, false);
5722
5723 // Right and middle button click support.
5724 link.addEventListener("mousedown", function(event) {
5725 if (event.button === 1)
5726 bbb.blacklist.smart_view_target = link;
5727 }, false);
5728 link.addEventListener("mouseup", function(event) {
5729 if (event.button === 1 && bbb.blacklist.smart_view_target === link)
5730 blacklistSmartViewUpdate(el);
5731 else if (event.button === 2)
5732 blacklistSmartViewUpdate(el);
5733 }, false);
5734 }
5735
5736 function blacklistSmartViewUpdate(el) {
5737 // Update the blacklisted thumbnail info in the smart view object.
5738 var time = new Date().getTime();
5739 var id = el.getAttribute("data-id");
5740 var smartView = localStorage.getItem("bbb_smart_view");
5741
5742 if (smartView === null) // Initialize the object if it doesn't exist.
5743 smartView = {last: time};
5744 else {
5745 smartView = JSON.parse(smartView);
5746
5747 if (time - smartView.last > 60000) // Reset the object if it hasn't been changed within a minute.
5748 smartView = {last: time};
5749 else
5750 smartView.last = time; // Adjust the object.
5751 }
5752
5753 if (!el.bbbHasClass("blacklisted-active"))
5754 smartView[id] = time;
5755 else
5756 delete smartView[id];
5757
5758 localStorage.bbbSetItem("bbb_smart_view", JSON.stringify(smartView));
5759 }
5760
5761 function blacklistSmartViewCheck(id) {
5762 // Check whether to display the post during the blacklist init.
5763 var smartView = localStorage.getItem("bbb_smart_view");
5764
5765 if (!blacklist_smart_view || smartView === null)
5766 return false;
5767 else {
5768 var time = new Date().getTime();
5769
5770 smartView = JSON.parse(smartView);
5771
5772 if (time - smartView.last > 60000) { // Delete the ids if the object hasn't been changed within a minute.
5773 localStorage.removeItem("bbb_smart_view");
5774 return false;
5775 }
5776 else if (!smartView[id]) // Return false if the id isn't found.
5777 return false;
5778 else if (time - smartView[id] > 60000) // Return false if the click is over a minute ago.
5779 return false;
5780 }
5781
5782 return true;
5783 }
5784
5785 /* Other functions */
5786 function modifyDanbScript() {
5787 // Modify some Danbooru functions so that they don't run unnecessarily.
5788 var loadNotes = Danbooru.Note.load_all;
5789
5790 Danbooru.Note.load_all = function(allow) {
5791 if (allow === "bbb")
5792 loadNotes();
5793 };
5794
5795 Danbooru.Blacklist.initialize_all = function() {
5796 return;
5797 };
5798 }
5799
5800 function modifyPage() {
5801 // Determine what function may be needed to fix/update content.
5802 if (noXML())
5803 return;
5804
5805 if (gLoc === "post")
5806 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.
5807 else if (gLoc === "comment_search" || gLoc === "comment")
5808 delayMe(fixCommentSearch);
5809 else if (useAPI()) // API only features.
5810 searchJSON(gLoc);
5811 else // Alternate mode for features.
5812 searchPages(gLoc);
5813 }
5814
5815 function formatInfo(post) {
5816 // Add information to/alter information in the post object.
5817 if (!post)
5818 return undefined;
5819
5820 // Figure out the thumbnail classes.
5821 var flags = "";
5822 var thumbClass = "";
5823
5824 if (post.is_deleted) {
5825 flags += " deleted";
5826 thumbClass += " post-status-deleted";
5827 }
5828 if (post.is_pending) {
5829 flags += " pending";
5830 thumbClass += " post-status-pending";
5831 }
5832 if (post.is_banned)
5833 flags += " banned";
5834 if (post.is_flagged) {
5835 flags += " flagged";
5836 thumbClass += " post-status-flagged";
5837 }
5838 if (post.has_children && (post.has_active_children || show_deleted))
5839 thumbClass += " post-status-has-children";
5840 if (post.parent_id)
5841 thumbClass += " post-status-has-parent";
5842
5843 // Figure out sample image dimensions and ratio.
5844 post.sample_ratio = (post.image_width > 850 ? 850 / post.image_width : 1);
5845 post.sample_height = Math.round(post.image_height * post.sample_ratio);
5846 post.sample_width = Math.round(post.image_width * post.sample_ratio);
5847
5848 // Hidden post fixes.
5849 post.md5 = post.md5 || "";
5850 post.file_ext = post.file_ext || "";
5851 post.preview_file_url = post.preview_file_url || bbbHiddenImg;
5852 post.large_file_url = post.large_file_url || "";
5853 post.file_url = post.file_url || "";
5854
5855 // Potential null value fixes.
5856 post.approver_id = post.approver_id || "";
5857 post.parent_id = post.parent_id || "";
5858 post.pixiv_id = post.pixiv_id || "";
5859
5860 // Missing API/data fixes.
5861 post.has_sound = (typeof(post.has_sound) === "boolean" ? post.has_sound : /(?:^|\s)(?:video|flash)_with_sound(?:$|\s)/.test(post.tag_string));
5862
5863 post.flags = flags.bbbSpaceClean();
5864 post.thumb_class = thumbClass;
5865
5866 return post;
5867 }
5868
5869 function fixPaginator(target) {
5870 // Determine whether the paginator needs to be updated and request one as needed.
5871 var paginator = getPaginator(target);
5872
5873 if (!paginator || gLoc === "pool" || gLoc === "favorite_group" || !allowUserLimit())
5874 return;
5875
5876 if (/\d/.test(paginator.textContent)) { // Fix numbered paginators.
5877 // Fix existing paginator with user's custom limit.
5878 var pageLinks = paginator.getElementsByTagName("a");
5879 var pageLink;
5880
5881 for (var i = 0, il = pageLinks.length; i < il; i++) {
5882 pageLink = pageLinks[i];
5883 pageLink.href = updateURLQuery(pageLink.href, {limit: thumbnail_count});
5884 }
5885
5886 searchPages("paginator");
5887 }
5888 else { // Fix next/previous paginators.
5889 paginator.innerHTML = "<p>Loading...</p>"; // Disable the paginator while fixing it.
5890
5891 searchPages("paginator");
5892 }
5893 }
5894
5895 function fixCommentSearch() {
5896 // Fix the thumbnails for hidden posts in the comment search.
5897 var posts = getPosts();
5898
5899 for (var i = 0, il = posts.length; i < il; i++) {
5900 var post = posts[i];
5901 var hasImg = post.getElementsByTagName("img")[0];
5902 var tags = post.getAttribute("data-tags");
5903 var previewUrl = post.getAttribute("data-preview-file-url");
5904
5905 // If the information for fixing the thumbnails is missing, stop checking.
5906 if (!hasImg && !previewUrl)
5907 return;
5908
5909 // Skip posts with content the user doesn't want or that already have images.
5910 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(post.getAttribute("data-flags"))) || safebPostTest(post))
5911 continue;
5912
5913 var preview = post.getElementsByClassName("preview")[0];
5914 var before = preview.firstElementChild;
5915
5916 var thumb = document.createElement("a");
5917 thumb.href = "/posts/" + post.getAttribute("data-id");
5918 thumb.innerHTML = '<img src="' + previewUrl + '" alt="' + post.getAttribute("data-md5") + '" title="' + tags + ' user:' + post.getAttribute("data-uploader") + ' rating:' + post.getAttribute("data-rating") + ' score:' + post.getAttribute("data-score") + '">';
5919
5920 if (before)
5921 preview.insertBefore(thumb, before);
5922 else
5923 preview.appendChild(thumb);
5924
5925 prepThumbnails(post);
5926 }
5927 }
5928
5929 function bbbNotice(txt, noticeType) {
5930 // Display the notice or add information to it if it already exists.
5931 // 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
5932 var notice = bbb.el.notice;
5933 var noticeMsg = bbb.el.noticeMsg;
5934 var type = (typeof(noticeType) !== "undefined" ? noticeType : 6);
5935
5936 var msg = document.createElement("div");
5937 msg.className = "bbb-notice-msg-entry";
5938 msg.innerHTML = txt;
5939 msg.style.color = (type === -1 ? "#FF0000" : "#000000");
5940
5941 if (!notice) {
5942 var noticeContainer = document.createElement("span"); // Will contain the Danbooru notice and BBB notice so that they can "stack" and reposition as the other disappears.
5943 noticeContainer.id = "bbb-notice-container";
5944
5945 var danbNotice = document.getElementById("notice");
5946
5947 // Override Danbooru notice styling to make it get along with the notice container.
5948 if (danbNotice) {
5949 danbNotice.style.marginBottom = "10px";
5950 danbNotice.style.width = "100%";
5951 danbNotice.style.position = "relative";
5952 danbNotice.style.top = "0px";
5953 danbNotice.style.left = "0px";
5954 noticeContainer.appendChild(danbNotice);
5955 }
5956
5957 notice = bbb.el.notice = document.createElement("div");
5958 notice.id = "bbb-notice";
5959 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>';
5960 noticeContainer.appendChild(notice);
5961
5962 noticeMsg = bbb.el.noticeMsg = getId("bbb-notice-msg", notice);
5963
5964 getId("bbb-notice-close", notice).addEventListener("click", function(event) {
5965 closeBbbNotice();
5966 event.preventDefault();
5967 }, false);
5968
5969 document.body.appendChild(noticeContainer);
5970 }
5971
5972 if (bbb.timers.keepBbbNotice)
5973 window.clearTimeout(bbb.timers.keepBbbNotice);
5974
5975 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.
5976 noticeMsg.insertBefore(msg, noticeMsg.firstElementChild);
5977
5978 // Don't allow the notice to be closed via clicking for half a second. Prevents accidental message closing.
5979 bbb.timers.keepBbbNotice = window.setTimeout(function() {
5980 bbb.timers.keepBbbNotice = 0;
5981 }, 500);
5982 }
5983 else { // Make sure the notice is clear and put in the first message.
5984 noticeMsg.innerHTML = "";
5985 noticeMsg.appendChild(msg);
5986 }
5987
5988 // Hide the notice after a certain number of seconds.
5989 if (type > 0) {
5990 window.setTimeout(function() {
5991 closeBbbNoticeMsg(msg);
5992 }, type * 1000);
5993 }
5994
5995 notice.style.display = "block";
5996
5997 return msg;
5998 }
5999
6000 function closeBbbNotice() {
6001 // Click handler for closing the notice.
6002 if (bbb.timers.keepBbbNotice)
6003 return;
6004
6005 bbb.el.notice.style.display = "none";
6006 }
6007
6008 function closeBbbNoticeMsg(el) {
6009 // Closes the provided notice message or the whole notice if there is only one message.
6010 var notice = bbb.el.notice;
6011 var target = el;
6012 var targetParent = target.parentNode;
6013
6014 if (notice.getElementsByClassName("bbb-notice-msg-entry").length < 2)
6015 closeBbbNotice();
6016 else if (targetParent)
6017 targetParent.removeChild(target);
6018 }
6019
6020 function bbbStatus(mode, xmlState) {
6021 // Updates the BBB status message.
6022 // xmlState: "new" = opening an XML request, "done" = closing an xml request, "error" = xml request failed.
6023 if (!enable_status_message)
6024 return;
6025
6026 var status = bbb.el.status;
6027
6028 // Set up the status message if it isn't ready.
6029 if (!status) {
6030 bbb.status = { // Status messages.
6031 msg: {
6032 post_comments: {txt: "Fixing hidden comments... ", count: 0},
6033 hidden: {txt: "Fixing hidden thumbnails... ", count: 0, queue: document.getElementsByClassName("bbb-hidden-thumb")}, // Hidden thumbnail message.
6034 posts: {txt: "Loading post info... ", count: 0} // General message for XML requests for hidden posts.
6035 },
6036 count: 0
6037 };
6038
6039 var msgList = bbb.status.msg;
6040
6041 status = bbb.el.status = document.createElement("div");
6042 status.id = "bbb-status";
6043
6044 for (var i in msgList) {
6045 if (msgList.hasOwnProperty(i)) {
6046 var curMsg = msgList[i];
6047
6048 var msgDiv = curMsg.el = document.createElement("div");
6049 msgDiv.style.display = "none";
6050 msgDiv.innerHTML = curMsg.txt;
6051 status.appendChild(msgDiv);
6052
6053 curMsg.info = document.createElement("span");
6054 msgDiv.appendChild(curMsg.info);
6055 }
6056 }
6057
6058 document.body.appendChild(status);
6059 }
6060
6061 var msg = bbb.status.msg[mode];
6062 var newCount = 0;
6063
6064 if (msg.queue) { // If the xml requests are queued, use the queue length as the current remaining value.
6065 newCount = (xmlState === "error" ? 0 : msg.queue.length);
6066 bbb.status.count += newCount - msg.count;
6067 msg.count = newCount;
6068 msg.info.innerHTML = newCount; // Update the displayed number of requests remaining.
6069 }
6070 else { // For simultaneous xml requests, just increment/decrement.
6071 if (xmlState === "new")
6072 newCount = 1;
6073 else if (xmlState === "done" || xmlState === "error")
6074 newCount = -1;
6075
6076 bbb.status.count += newCount;
6077 msg.count += newCount;
6078 }
6079
6080 if (msg.count)
6081 msg.el.style.display = "block";
6082 else
6083 msg.el.style.display = "none";
6084
6085 if (bbb.status.count) // If requests are pending, display the notice.
6086 status.style.display = "block";
6087 else // If requests are done, hide the notice.
6088 status.style.display = "none";
6089 }
6090
6091 function bbbDialog(content, properties) {
6092 // 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:
6093 // ok/cancel: true to display the button, false to hide the button, function to display the button and specify a custom function for it
6094 // 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
6095 // 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
6096
6097 var prop = properties || {};
6098 var okButton = (prop.ok === undefined ? true : prop.ok);
6099 var cancelButton = (prop.cancel === undefined ? false : prop.cancel);
6100 var condition = (prop.condition === undefined ? false : prop.condition);
6101 var important = (prop.important === undefined ? false : prop.important);
6102
6103 // Queue the dialog window if one is already open.
6104 if (document.getElementById("bbb-dialog-blocker")) {
6105 if (important)
6106 bbb.dialog.queue.unshift({content: content, properties: properties});
6107 else
6108 bbb.dialog.queue.push({content: content, properties: properties});
6109
6110 return;
6111 }
6112
6113 // Test whether the dialog window should be allowed to display.
6114 if (condition) {
6115 var conditionType = typeof(condition);
6116
6117 if ((conditionType === "string" && bbb.flags[condition]) || (conditionType === "function" && condition())) {
6118 nextBbbDialog();
6119 return;
6120 }
6121 else if (conditionType === "string")
6122 bbb.flags[condition] = true;
6123 }
6124
6125 // Create the dialog window.
6126 var blockDiv = document.createElement("div");
6127 blockDiv.id = "bbb-dialog-blocker";
6128
6129 var windowDiv = document.createElement("div");
6130 windowDiv.id = "bbb-dialog-window";
6131 windowDiv.tabIndex = "-1";
6132 blockDiv.appendChild(windowDiv);
6133
6134 var contentDiv = windowDiv;
6135
6136 if (okButton) {
6137 var ok = document.createElement("a");
6138 ok.innerHTML = "OK";
6139 ok.href = "#";
6140 ok.className = "bbb-dialog-button";
6141
6142 if (typeof(okButton) === "function")
6143 ok.addEventListener("click", okButton, false);
6144
6145 ok.addEventListener("click", closeBbbDialog, false);
6146
6147 okButton = ok;
6148 }
6149
6150 if (cancelButton) {
6151 var cancel = document.createElement("a");
6152 cancel.innerHTML = "Cancel";
6153 cancel.href = "#";
6154 cancel.className = "bbb-dialog-button";
6155 cancel.style.cssFloat = "right";
6156
6157 if (typeof(cancelButton) === "function")
6158 cancel.addEventListener("click", cancelButton, false);
6159
6160 cancel.addEventListener("click", closeBbbDialog, false);
6161
6162 cancelButton = cancel;
6163 }
6164
6165 if (okButton || cancelButton) {
6166 contentDiv = document.createElement("div");
6167 contentDiv.className = "bbb-dialog-content-div";
6168 windowDiv.appendChild(contentDiv);
6169
6170 var buttonDiv = document.createElement("div");
6171 buttonDiv.className = "bbb-dialog-button-div";
6172 windowDiv.appendChild(buttonDiv);
6173
6174 if (okButton)
6175 buttonDiv.appendChild(okButton);
6176
6177 if (cancelButton)
6178 buttonDiv.appendChild(cancelButton);
6179 }
6180
6181 if (typeof(content) === "string")
6182 contentDiv.innerHTML = content;
6183 else
6184 contentDiv.appendChild(content);
6185
6186 document.body.appendChild(blockDiv);
6187
6188 (okButton || cancelButton || windowDiv).focus();
6189 }
6190
6191 function closeBbbDialog(event) {
6192 // Close the current dialog window.
6193 var dialogBlocker = document.getElementById("bbb-dialog-blocker");
6194
6195 if (dialogBlocker)
6196 document.body.removeChild(dialogBlocker);
6197
6198 nextBbbDialog();
6199
6200 event.preventDefault();
6201 }
6202
6203 function nextBbbDialog() {
6204 // Open the next queued dialog window.
6205 var nextDialog = bbb.dialog.queue.shift();
6206
6207 if (nextDialog)
6208 bbbDialog(nextDialog.content, nextDialog.properties);
6209 }
6210
6211 function thumbSearchMatch(post, searchArray) {
6212 // Take search objects and test them against a thumbnail's info.
6213 if (!searchArray[0])
6214 return false;
6215
6216 var postInfo;
6217
6218 if (post instanceof Element) {
6219 var tags = post.getAttribute("data-tags");
6220 var flags = post.getAttribute("data-flags") || "active";
6221 var rating = " rating:" + post.getAttribute("data-rating");
6222 var status = " status:" + (flags === "flagged" ? flags + " active" : flags).replace(/\s/g, " status:");
6223 var user = " user:" + post.getAttribute("data-uploader").replace(/\s/g, "_").toLowerCase();
6224 var poolData = " " + post.getAttribute("data-pools");
6225 var pools = (/pool:\d+/.test(poolData) && !/pool:(collection|series)/.test(poolData) ? poolData + " pool:inactive" : poolData);
6226 var score = post.getAttribute("data-score");
6227 var favcount = post.getAttribute("data-fav-count");
6228 var id = post.getAttribute("data-id");
6229 var width = post.getAttribute("data-width");
6230 var height = post.getAttribute("data-height");
6231 var parentId = post.getAttribute("data-parent-id");
6232 var parent = (parentId ? " parent:" + parentId : "");
6233 var hasChildren = post.getAttribute("data-has-children");
6234 var child = (hasChildren === "true" ? " child:true" : "");
6235
6236 postInfo = {
6237 tags: tags.bbbSpacePad(),
6238 metatags:(rating + status + user + pools + parent + child).bbbSpacePad(),
6239 score: Number(score),
6240 favcount: Number(favcount),
6241 id: Number(id),
6242 width: Number(width),
6243 height: Number(height)
6244 };
6245 }
6246 else
6247 postInfo = post;
6248
6249 var anyResult;
6250 var allResult;
6251 var searchTerm = "";
6252 var j, jl; // Loop variables.
6253
6254 for (var i = 0, il = searchArray.length; i < il; i++) {
6255 var searchObject = searchArray[i];
6256 var all = searchObject.all;
6257 var any = searchObject.any;
6258
6259 // Continue to the next matching rule if there are no tags to test.
6260 if (!any.total && !all.total)
6261 continue;
6262
6263 if (any.total) {
6264 anyResult = false;
6265
6266 // Loop until one positive match is found.
6267 for (j = 0, jl = any.includes.length; j < jl; j++) {
6268 searchTerm = any.includes[j];
6269
6270 if (thumbTagMatch(postInfo, searchTerm)) {
6271 anyResult = true;
6272 break;
6273 }
6274 }
6275
6276 // If we don't have a positive match yet, loop through the excludes.
6277 if (!anyResult) {
6278 for (j = 0, jl = any.excludes.length; j < jl; j++) {
6279 searchTerm = any.excludes[j];
6280
6281 if (!thumbTagMatch(postInfo, searchTerm)) {
6282 anyResult = true;
6283 break;
6284 }
6285 }
6286 }
6287
6288 // Continue to the next matching rule if none of the "any" tags matched.
6289 if (!anyResult)
6290 continue;
6291 }
6292
6293 if (all.total) {
6294 allResult = true;
6295
6296 // Loop until a negative match is found.
6297 for (j = 0, jl = all.includes.length; j < jl; j++) {
6298 searchTerm = all.includes[j];
6299
6300 if (!thumbTagMatch(postInfo, searchTerm)) {
6301 allResult = false;
6302 break;
6303 }
6304 }
6305
6306 // If we still have a positive match, loop through the excludes.
6307 if (allResult) {
6308 for (j = 0, jl = all.excludes.length; j < jl; j++) {
6309 searchTerm = all.excludes[j];
6310
6311 if (thumbTagMatch(postInfo, searchTerm)) {
6312 allResult = false;
6313 break;
6314 }
6315 }
6316 }
6317
6318 // Continue to the next matching rule if one of the "all" tags didn't match.
6319 if (!allResult)
6320 continue;
6321 }
6322
6323 // Loop completed without a negative match so return true.
6324 return true;
6325 }
6326
6327 // If we haven't managed a positive match for any rules, return false.
6328 return false;
6329 }
6330
6331 function thumbTagMatch(postInfo, tag) {
6332 // Test thumbnail info for a tag match.
6333 var targetTags;
6334
6335 if (typeof(tag) === "string") { // Check regular tags and metatags with string values.
6336 targetTags = (isMetatag(tag) ? postInfo.metatags : postInfo.tags);
6337
6338 if (targetTags.indexOf(tag) > -1)
6339 return true;
6340 else
6341 return false;
6342 }
6343 else if (tag instanceof RegExp) { // Check wildcard tags.
6344 targetTags = (isMetatag(tag.source) ? postInfo.metatags : postInfo.tags);
6345
6346 return tag.test(targetTags);
6347 }
6348 else if (typeof(tag) === "object") {
6349 if (tag instanceof Array) // Check grouped tags.
6350 return thumbSearchMatch(postInfo, tag);
6351 else { // Check numeric metatags.
6352 var tagsMetaValue = postInfo[tag.tagName];
6353
6354 if (tag.equals !== undefined) {
6355 if (tagsMetaValue !== tag.equals)
6356 return false;
6357 }
6358 else {
6359 if (tag.greater !== undefined && tagsMetaValue <= tag.greater)
6360 return false;
6361
6362 if (tag.less !== undefined && tagsMetaValue >= tag.less)
6363 return false;
6364 }
6365
6366 return true;
6367 }
6368 }
6369 }
6370
6371 function createSearch(search) {
6372 // Take search strings, turn them into search objects, and pass back the objects in an array.
6373 if (!/[^\s,]/.test(search))
6374 return [];
6375
6376 var groupsObject = replaceSearchGroups(search);
6377 var groups = groupsObject.groups;
6378 var searchStrings = groupsObject.search.toLowerCase().replace(/\b(rating:[qes])\w+/g, "$1").split(",");
6379 var searches = [];
6380
6381 // Sort through each matching rule.
6382 for (var i = 0, il = searchStrings.length; i < il; i++) {
6383 var searchString = searchStrings[i].split(" ");
6384 var searchObject = {
6385 all: {includes: [], excludes: [], total: 0},
6386 any: {includes: [], excludes: [], total: 0}
6387 };
6388
6389 // Divide the tags into any and all sets with excluded and included tags.
6390 for (var j = 0, jl = searchString.length; j < jl; j++) {
6391 var searchTerm = searchString[j];
6392 var mode;
6393 var primaryMode = "all";
6394 var secondaryMode = "includes";
6395
6396 while (searchTerm.charAt(0) === "~" || searchTerm.charAt(0) === "-") {
6397 switch (searchTerm.charAt(0)) {
6398 case "~":
6399 primaryMode = "any";
6400 break;
6401 case "-":
6402 secondaryMode = "excludes";
6403 break;
6404 }
6405
6406 searchTerm = searchTerm.slice(1);
6407 }
6408
6409 if (!searchTerm.length) // Stop if there is no actual tag.
6410 continue;
6411
6412 mode = searchObject[primaryMode][secondaryMode];
6413
6414 if (isNumMetatag(searchTerm)) { // Parse numeric metatags and turn them into objects.
6415 var tagArray = searchTerm.split(":");
6416 var metaObject = {
6417 tagName: tagArray[0],
6418 equals: undefined,
6419 greater: undefined,
6420 less: undefined
6421 };
6422 var numSearch = tagArray[1];
6423 var numArray;
6424 var equals;
6425 var greater;
6426 var less;
6427
6428 if (numSearch.indexOf("<=") === 0 || numSearch.indexOf("..") === 0) { // Less than or equal to. (tag:<=# & tag:..#)
6429 less = parseInt(numSearch.slice(2), 10);
6430
6431 if (!isNaN(less)) {
6432 metaObject.less = less + 1;
6433 mode.push(metaObject);
6434 }
6435 }
6436 else if (numSearch.indexOf(">=") === 0) { // Greater than or equal to. (tag:>=#)
6437 greater = parseInt(numSearch.slice(2), 10);
6438
6439 if (!isNaN(greater)) {
6440 metaObject.greater = greater - 1;
6441 mode.push(metaObject);
6442 }
6443 }
6444 else if (numSearch.length > 2 && numSearch.indexOf("..") === numSearch.length - 2) { // Greater than or equal to. (tag:#..)
6445 greater = parseInt(numSearch.slice(0, -2), 10);
6446
6447 if (!isNaN(greater)) {
6448 metaObject.greater = greater - 1;
6449 mode.push(metaObject);
6450 }
6451 }
6452 else if (numSearch.charAt(0) === "<") { // Less than. (tag:<#)
6453 less = parseInt(numSearch.slice(1), 10);
6454
6455 if (!isNaN(less)) {
6456 metaObject.less = less;
6457 mode.push(metaObject);
6458 }
6459 }
6460 else if (numSearch.charAt(0) === ">") { // Greater than. (tag:>#)
6461 greater = parseInt(numSearch.slice(1), 10);
6462
6463 if (!isNaN(greater)) {
6464 metaObject.greater = greater;
6465 mode.push(metaObject);
6466 }
6467 }
6468 else if (numSearch.indexOf("..") > -1) { // Greater than or equal to and less than or equal to range. (tag:#..#)
6469 numArray = numSearch.split("..");
6470 greater = parseInt(numArray[0], 10);
6471 less = parseInt(numArray[1], 10);
6472
6473 if (!isNaN(greater) && !isNaN(less)) {
6474 metaObject.greater = greater - 1;
6475 metaObject.less = less + 1;
6476 mode.push(metaObject);
6477 }
6478 }
6479 else { // Exact number. (tag:#)
6480 equals = parseInt(numSearch, 10);
6481
6482 if (!isNaN(equals)) {
6483 metaObject.equals = equals;
6484 mode.push(metaObject);
6485 }
6486 }
6487 }
6488 else if (searchTerm.indexOf("*") > -1) // Prepare wildcard tags as regular expressions.
6489 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*".
6490 else if (/%\d+%/.test(searchTerm)) { // Prepare grouped tags as a search object.
6491 var groupIndex = Number(searchTerm.match(/\d+/)[0]);
6492
6493 mode.push(createSearch(groups[groupIndex]));
6494 }
6495 else if (typeof(searchTerm) === "string") { // Add regular tags.
6496 if (isMetatag(searchTerm)) {
6497 var tagObject = searchTerm.split(/:(.+)/, 2);
6498 var tagName = tagObject[0];
6499 var tagValue = tagObject[1];
6500
6501 // Drop metatags with no value.
6502 if (!tagValue)
6503 continue;
6504
6505 if (tagValue === "any" && (tagName === "pool" || tagName === "parent" || tagName === "child"))
6506 mode.push(new RegExp((tagName + ":\\S*").bbbSpacePad()));
6507 else if (tagValue === "none" && (tagName === "pool" || tagName === "parent" || tagName === "child")) {
6508 secondaryMode = (secondaryMode === "includes" ? "excludes" : "includes"); // Flip the include/exclude mode.
6509 mode = searchObject[primaryMode][secondaryMode];
6510
6511 mode.push(new RegExp((tagName + ":\\S*").bbbSpacePad()));
6512 }
6513 else if (tagValue === "active" && tagName === "pool")
6514 mode.push(new RegExp((tagName + ":(collection|series)").bbbSpacePad()));
6515 else // Allow all other values through (ex: parent:# & pool:series).
6516 mode.push(searchTerm.bbbSpacePad());
6517 }
6518 else
6519 mode.push(searchTerm.bbbSpacePad());
6520 }
6521 }
6522
6523 searchObject.all.total = searchObject.all.includes.length + searchObject.all.excludes.length;
6524 searchObject.any.total = searchObject.any.includes.length + searchObject.any.excludes.length;
6525
6526 if (searchObject.all.total || searchObject.any.total)
6527 searches.push(searchObject);
6528 }
6529
6530 return searches;
6531 }
6532
6533 function replaceSearchGroups(search) {
6534 // Collect all the nested/grouped tags in a search and replace them with placeholders.
6535 if (search.indexOf("%") < 0)
6536 return {search: search, groups: []};
6537
6538 var searchString = search;
6539 var parens = searchString.match(/\(%|%\)/g);
6540
6541 // Remove unpaired opening parentheses near the end of the search.
6542 while (parens[parens.length - 1] === "(%") {
6543 searchString = searchString.replace(/^(.*\s)?[~-]*\(%/, "$1");
6544 parens.pop();
6545 }
6546
6547 // Take the remaining parentheses and figure out how to pair them up.
6548 var startCount = 0;
6549 var endCount = 0;
6550 var groupStartIndex = 0;
6551 var groups = [];
6552
6553 for (var i = 0, il = parens.length; i < il; i++) {
6554 var paren = parens[i];
6555 var nextParen = parens[i + 1];
6556
6557 if (paren === "(%")
6558 startCount++;
6559 else
6560 endCount++;
6561
6562 if (endCount > startCount) { // Remove unpaired closing parentheses near the start of the string.
6563 searchString = searchString.replace(/^(.*?)%\)/, "$1");
6564 endCount = 0;
6565 groupStartIndex++;
6566 }
6567 else if (startCount === endCount || (!nextParen && endCount > 0 && startCount > endCount)) { // Replace evenly paired parentheses with a placeholder.
6568 var groupRegex = new RegExp(parens.slice(groupStartIndex, i + 1).join(".*?").replace(/[\(\)]/g, "\\$&"));
6569 var groupMatch = searchString.match(groupRegex)[0];
6570
6571 searchString = searchString.replace(groupMatch, "%" + groups.length + "%");
6572 startCount = 0;
6573 endCount = 0;
6574 groupStartIndex = i + 1;
6575 groups.push(groupMatch.substring(2, groupMatch.length - 2));
6576 }
6577 else if (!nextParen && startCount > 0 && endCount === 0 ) // Remove leftover unpaired opening parentheses.
6578 searchString = searchString.replace(/^(.*\s)?[~-]*\(%/, "$1");
6579 }
6580
6581 return {search: searchString, groups: groups};
6582 }
6583
6584 function restoreSearchGroups(search, groups) {
6585 // Replace all group placeholders with their corresponding group.
6586 var restoredSearch = search;
6587
6588 for (var i = 0, il = groups.length; i < il; i++) {
6589 var groupPlaceholder = new RegExp("%" + i + "%");
6590
6591 restoredSearch = restoredSearch.replace(groupPlaceholder, "(%" + groups[i] + "%)");
6592 }
6593
6594 return restoredSearch;
6595 }
6596
6597 function cleanSearchGroups(string) {
6598 // Take a search string and clean up extra spaces, commas, and any parentheses that are missing their opening/closing parenthesis.
6599 var groupObject = replaceSearchGroups(string);
6600 var groups = groupObject.groups;
6601 var searchString = groupObject.search;
6602
6603 for (var i = 0, il = groups.length; i < il; i++)
6604 groups[i] = cleanSearchGroups(groups[i]);
6605
6606 searchString = restoreSearchGroups(searchString, groups).bbbTagClean();
6607
6608 return searchString;
6609 }
6610
6611 function searchSingleToMulti(string) {
6612 // Take a single line search and format it into multiple lines for a textarea.
6613 var groupsObject = replaceSearchGroups(cleanSearchGroups(string));
6614 var searchString = groupsObject.search;
6615 var groups = groupsObject.groups;
6616 var searchText = searchString.replace(/,\s*/g, "\r\n\r\n");
6617
6618 searchText = restoreSearchGroups(searchText, groups);
6619
6620 return searchText;
6621 }
6622
6623 function searchMultiToSingle(multi) {
6624 // Take a multiple line search from a textarea and format it into a single line.
6625 var searchStrings = multi.split(/[\r\n]+/g);
6626
6627 for (var i = 0, il = searchStrings.length; i < il; i++)
6628 searchStrings[i] = cleanSearchGroups(searchStrings[i]);
6629
6630 var searchString = searchStrings.join(", ");
6631
6632 return searchString;
6633 }
6634
6635 function trackNew() {
6636 // Set up the track new option and manage the search.
6637 var header = document.getElementById("top");
6638
6639 if (!track_new || !header)
6640 return;
6641
6642 var activeMenu = header.getElementsByClassName("current")[0];
6643 var secondMenu = header.getElementsByTagName("menu")[1];
6644
6645 // Insert new posts link.
6646 if (activeMenu && activeMenu.textContent === "Posts" && secondMenu) {
6647 var menuItems = secondMenu.getElementsByTagName("li");
6648 var numMenuItems = secondMenu.getElementsByTagName("li").length;
6649 var listingItemSibling = menuItems[1];
6650
6651 for (var i = 0; i < numMenuItems; i++) {
6652 var menuLink = menuItems[i];
6653 var nextLink = menuItems[i + 1];
6654
6655 if (menuLink.textContent.indexOf("Listing") > -1) {
6656 if (nextLink)
6657 listingItemSibling = nextLink;
6658 else
6659 listingItemSibling = undefined;
6660
6661 break;
6662 }
6663 }
6664
6665 var link = document.createElement("a");
6666 link.href = "/posts?new_posts=redirect&page=b1";
6667 link.innerHTML = "New";
6668 link.addEventListener("click", function(event) {
6669 if (event.button === 0) {
6670 trackNewLoad();
6671 event.preventDefault();
6672 }
6673 }, false);
6674
6675 var item = document.createElement("li");
6676 item.appendChild(link);
6677
6678 if (listingItemSibling)
6679 secondMenu.insertBefore(item, listingItemSibling);
6680 else
6681 secondMenu.appendChild(item);
6682 }
6683
6684 if (gLoc === "search") {
6685 var info = track_new_data;
6686 var mode = getVar("new_posts");
6687 var postsDiv = document.getElementById("posts");
6688 var postSections = document.getElementById("post-sections");
6689 var posts = getPosts();
6690 var firstPost = posts[0];
6691
6692 if (mode === "init" && !info.viewed && !getVar("tags") && !getVar("page")) { // Initialize.
6693 if (firstPost) {
6694 info.viewed = Number(firstPost.getAttribute("data-id"));
6695 info.viewing = 1;
6696 saveSettings();
6697 bbbNotice("New post tracking initialized. Tracking will start with new posts after the current last image.", 8);
6698 }
6699 }
6700 else if (mode === "redirect") { // Bookmarkable redirect link. (http://danbooru.donmai.us/posts?new_posts=redirect&page=b1)
6701 if (postsDiv)
6702 postsDiv.innerHTML = "<b>Redirecting...</b>";
6703
6704 trackNewLoad();
6705 }
6706 else if (mode === "list") {
6707 var limitNum = getLimit() || thumbnail_count || thumbnail_count_default;
6708 var currentPage = Number(getVar("page")) || 1;
6709 var savedPage = Math.ceil((info.viewing - limitNum) / limitNum) + 1;
6710 var currentViewed = Number(/id:>(\d+)/.exec(decodeURIComponent(location.search))[1]);
6711 var paginator = getPaginator();
6712
6713 // Replace the chickens message on the first page with a more specific message.
6714 if (!firstPost && currentPage < 2) {
6715 if (postsDiv && postsDiv.firstElementChild)
6716 postsDiv.firstElementChild.innerHTML = "No new posts.";
6717 }
6718
6719 // Update the saved page information.
6720 if (savedPage !== currentPage && info.viewed === currentViewed) {
6721 info.viewing = (currentPage - 1) * limitNum + 1;
6722 saveSettings();
6723 }
6724
6725 // Modify new post searches with a mark as viewed link.
6726 if (postSections) {
6727 var markSection = document.createElement("li");
6728
6729 var markLink = document.createElement("a");
6730 markLink.innerHTML = (currentPage > 1 ? "Mark pages 1-" + currentPage + " viewed" : "Mark page 1 viewed");
6731 markLink.href = "#";
6732 markSection.appendChild(markLink);
6733 postSections.appendChild(markSection);
6734
6735 markLink.addEventListener("click", function(event) {
6736 trackNewMark();
6737 event.preventDefault();
6738 }, false);
6739
6740 var resetSection = document.createElement("li");
6741 resetSection.style.cssFloat = "right";
6742
6743 var resetLink = document.createElement("a");
6744 resetLink.innerHTML = "Reset (Mark all viewed)";
6745 resetLink.href = "#";
6746 resetLink.style.color = "#FF1100";
6747 resetSection.appendChild(resetLink);
6748 postSections.appendChild(resetSection);
6749
6750 resetLink.addEventListener("click", function(event) {
6751 trackNewReset();
6752 event.preventDefault();
6753 }, false);
6754
6755 // Update the mark link if the paginator updates.
6756 if (paginator) {
6757 paginator.bbbWatchNodes(function() {
6758 var activePage = paginator.getElementsByTagName("span")[0];
6759
6760 if (activePage)
6761 markLink.innerHTML = "Mark pages 1-" + activePage.textContent.bbbSpaceClean() + " viewed";
6762 });
6763 }
6764 }
6765 }
6766 }
6767 }
6768
6769 function trackNewLoad() {
6770 // Create the search URL and load it.
6771 var info = bbb.user.track_new_data;
6772 var limitNum = bbb.user.thumbnail_count || thumbnail_count_default;
6773 var savedPage = Math.ceil((info.viewing - limitNum) / limitNum) + 1;
6774
6775 if (info.viewed)
6776 location.href = "/posts?new_posts=list&tags=order:id_asc+id:>" + info.viewed + "&page=" + savedPage + "&limit=" + limitNum;
6777 else
6778 location.href = "/posts?new_posts=init&limit=" + limitNum;
6779 }
6780
6781 function trackNewReset() {
6782 // Reinitialize settings/Mark all viewed.
6783 loadSettings();
6784
6785 var limitNum = bbb.user.thumbnail_count || thumbnail_count_default;
6786
6787 bbb.user.track_new_data = bbb.options.track_new_data.def;
6788 saveSettings();
6789
6790 bbbNotice("Reinitializing new post tracking. Please wait.", 0);
6791 location.href = "/posts?new_posts=init&limit=" + limitNum;
6792 }
6793
6794 function trackNewMark() {
6795 // Mark the current images and older as viewed.
6796 loadSettings();
6797
6798 var info = bbb.user.track_new_data;
6799 var limitNum = getLimit() || bbb.user.thumbnail_count || thumbnail_count_default;
6800 var posts = getPosts();
6801 var lastPost = posts[posts.length - 1];
6802 var lastId = (lastPost ? Number(lastPost.getAttribute("data-id")) : null );
6803
6804 if (!lastPost)
6805 bbbNotice("Unable to mark as viewed. No posts detected.", -1);
6806 else if (info.viewed >= lastId)
6807 bbbNotice("Unable to mark as viewed. Posts have already been marked.", -1);
6808 else {
6809 info.viewed = Number(lastPost.getAttribute("data-id"));
6810 info.viewing = 1;
6811 saveSettings();
6812
6813 bbbNotice("Posts marked as viewed. Please wait while the pages are updated.", 0);
6814 location.href = "/posts?new_posts=list&tags=order:id_asc+id:>" + info.viewed + "&page=1&limit=" + limitNum;
6815 }
6816 }
6817
6818 function customCSS() {
6819 var i; // Loop variable.
6820 var customStyles = document.createElement("style");
6821 customStyles.type = "text/css";
6822
6823 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;}' +
6824 '#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.
6825 '#bbb-menu h1, #bbb-dialog-window h1 {font-size: 24px; line-height: 42px;}' +
6826 '#bbb-menu h2, #bbb-dialog-window h2 {font-size: 16px; line-height: 25px;}' +
6827 '#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;}' +
6828 '#bbb-menu input {height: 17px; padding: 1px 0px; margin-top: 4px; vertical-align: top;}' +
6829 '#bbb-menu input[type="checkbox"] {margin: 0px; vertical-align: middle; position: relative; bottom: 2px;}' +
6830 '#bbb-menu .bbb-general-input input[type="text"], #bbb-menu .bbb-general-input select {width: 175px;}' +
6831 '#bbb-menu select {height: 21px; margin-top: 4px; vertical-align: top;}' +
6832 '#bbb-menu option {padding: 0px 3px;}' +
6833 '#bbb-menu textarea, #bbb-dialog-window textarea {padding: 2px; resize: none;}' +
6834 '#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;}' +
6835 '#bbb-menu .bbb-scroll-div {border: 1px solid #CCCCCC; margin: -1px 0px 5px 0px; padding: 5px 0px; overflow-y: auto;}' +
6836 '#bbb-menu .bbb-page {position: relative; display: none;}' +
6837 '#bbb-menu .bbb-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 5px;}' +
6838 '#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;}' +
6839 '#bbb-menu .bbb-active-tab {background-color: #FFFFFF; border-bottom-width: 0px; padding-bottom: 6px;}' +
6840 '#bbb-menu .bbb-header {border-bottom: 2px solid #CCCCCC; margin-bottom: 5px; width: 700px;}' +
6841 '#bbb-menu .bbb-toc {list-style-type: upper-roman; margin-left: 30px;}' +
6842 '#bbb-menu .bbb-section-options, #bbb-menu .bbb-section-text {margin-bottom: 5px; max-width: 902px;}' +
6843 '#bbb-menu .bbb-section-options-left, #bbb-menu .bbb-section-options-right {display: inline-block; vertical-align: top; width: 435px;}' +
6844 '#bbb-menu .bbb-section-options-left {border-right: 1px solid #CCCCCC; margin-right: 15px; padding-right: 15px;}' +
6845 '#bbb-menu .bbb-general-label {display: block; height: 29px; padding: 0px 5px;}' +
6846 '#bbb-menu .bbb-general-label:hover {background-color: #EEEEEE;}' +
6847 '#bbb-menu .bbb-general-text {line-height: 29px;}' +
6848 '#bbb-menu .bbb-general-input {float: right; line-height: 29px;}' +
6849 '#bbb-menu .bbb-expl-link {font-size: 12px; font-weight: bold; margin-left: 5px; padding: 2px;}' +
6850 '#bbb-menu .bbb-border-div {background-color: #EEEEEE; padding: 2px; margin: 0px 5px 0px 0px;}' +
6851 '#bbb-menu .bbb-border-bar, #bbb-menu .bbb-border-settings {height: 29px; padding: 0px 2px; overflow: hidden;}' +
6852 '#bbb-menu .bbb-border-settings {background-color: #FFFFFF;}' +
6853 '#bbb-menu .bbb-border-div label, #bbb-menu .bbb-border-div span {display: inline-block; line-height: 29px;}' +
6854 '#bbb-menu .bbb-border-name {text-align: left; width: 540px;}' +
6855 '#bbb-menu .bbb-border-name input {width:460px;}' +
6856 '#bbb-menu .bbb-border-color {text-align: center; width: 210px;}' +
6857 '#bbb-menu .bbb-border-color input {width: 148px;}' +
6858 '#bbb-menu .bbb-border-style {float: right; text-align: right; width: 130px;}' +
6859 '#bbb-menu .bbb-border-divider {height: 4px;}' +
6860 '#bbb-menu .bbb-insert-highlight .bbb-border-divider {background-color: blue; cursor: pointer;}' +
6861 '#bbb-menu .bbb-no-highlight .bbb-border-divider {background-color: transparent; cursor: auto;}' +
6862 '#bbb-menu .bbb-border-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 2px; margin: 0px 2px;}' +
6863 '#bbb-menu .bbb-border-spacer {display: inline-block; height: 12px; width: 0px; border-right: 1px solid #CCCCCC; margin: 0px 5px;}' +
6864 '#bbb-menu .bbb-backup-area {height: 300px; width: 896px; margin-top: 2px;}' +
6865 '#bbb-menu .bbb-blacklist-area {height: 300px; width: 896px; margin-top: 2px;}' +
6866 '#bbb-menu .bbb-edit-link {background-color: #FFFFFF; border: 1px solid #CCCCCC; display: inline-block; height: 19px; line-height: 19px; margin-left: -1px; padding: 0px 2px; margin-top: 4px; text-align: center; vertical-align: top;}' +
6867 '#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);}' +
6868 '#bbb-expl * {font-size: 12px;}' +
6869 '#bbb-expl tiphead {display: block; font-weight: bold; text-decoration: underline; font-size: 13px; margin-top: 12px;}' +
6870 '#bbb-expl tipdesc {display: inline; font-weight: bold;}' +
6871 '#bbb-expl tipdesc:before {content: "\\A0"; display: block; height: 12px; clear: both;}' + // Simulate a double line break.
6872 '#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;}' +
6873 '#bbb-notice-container {position: fixed; top: 0.5em; left: 25%; width: 50%; z-index: 9002;}' +
6874 '#bbb-notice {padding: 3px; width: 100%; display: none; position: relative; border-radius: 2px; border: 1px solid #000000; background-color: #CCCCCC;}' +
6875 '#bbb-notice-msg {margin: 0px 25px 0px 55px; max-height: 200px; overflow: auto;}' +
6876 '#bbb-notice-msg .bbb-notice-msg-entry {border-bottom: solid 1px #000000; margin-bottom: 5px; padding-bottom: 5px;}' +
6877 '#bbb-notice-msg .bbb-notice-msg-entry:last-child {border-bottom: none 0px; margin-bottom: 0px; padding-bottom: 0px;}' +
6878 '#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;}' +
6879 '#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/
6880 '#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;}' +
6881 '#bbb-dialog-window .bbb-header {border-bottom: 2px solid #CCCCCC; margin-bottom: 5px; margin-right: 100px; padding-right: 50px; white-space: nowrap;}' +
6882 '#bbb-dialog-window .bbb-dialog-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 5px; margin: 0px 5px;}' +
6883 '#bbb-dialog-window .bbb-dialog-content-div {padding: 5px; overflow-x: hidden; overflow-y: auto;}' +
6884 '#bbb-dialog-window .bbb-dialog-button-div {padding-top: 10px; flex-grow: 0; flex-shrink: 0; overflow: hidden;}' +
6885 '#bbb-dialog-window .bbb-edit-area {height: 300px; width: 800px;}';
6886
6887 // Provide a little extra space for listings that allow thumbnail_count.
6888 if (thumbnail_count && (gLoc === "search" || gLoc === "notes" || gLoc === "favorites")) {
6889 styles += 'div#page {margin: 0px 10px 0px 20px !important;}' +
6890 'section#content {padding: 0px !important;}';
6891 }
6892
6893 // Calculate some dimensions.
6894 var totalBorderWidth = (custom_tag_borders ? border_width * 2 + (border_spacing * 2 || 1) : border_width + border_spacing);
6895 var thumbMaxWidth = 150 + totalBorderWidth * 2;
6896 var thumbMaxHeight = thumbMaxWidth;
6897 var listingExtraSpace = (14 - totalBorderWidth * 2 > 2 ? 14 - totalBorderWidth * 2 : 2);
6898 var commentExtraSpace = 34 - totalBorderWidth * 2;
6899 var customBorderSpacing = (border_spacing || 1);
6900
6901 if (thumb_info === "below")
6902 thumbMaxHeight += 18; // Add some extra height for the info.
6903
6904 // Border setup.
6905 var sbsl = status_borders.length;
6906 var statusBorderItem;
6907
6908 styles += 'article.post-preview a.bbb-thumb-link, .post-preview div.preview a.bbb-thumb-link {display: inline-block !important;}' +
6909 'article.post-preview {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin: 0px ' + listingExtraSpace + 'px ' + listingExtraSpace + 'px 0px !important;}' +
6910 'article.post-preview.pooled {height: ' + (thumbMaxHeight + 60) + 'px !important;}' + // Pool gallery view thumb height adjustment.
6911 '#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;}' +
6912 'article.post-preview a.bbb-thumb-link {line-height: 0px !important;}' +
6913 '.post-preview div.preview {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin-right: ' + commentExtraSpace + 'px !important;}' +
6914 '.post-preview div.preview a.bbb-thumb-link {line-height: 0px !important;}' +
6915 '.post-preview a.bbb-thumb-link img {border-width: ' + border_width + 'px !important; padding: ' + border_spacing + 'px !important;}' +
6916 'a.bbb-thumb-link.bbb-custom-tag {border-width: ' + border_width + 'px !important;}';
6917
6918 if (custom_status_borders) {
6919 var activeStatusStyles = "";
6920 var statusBorderInfo = {};
6921
6922 for (i = 0; i < sbsl; i++) {
6923 statusBorderItem = status_borders[i];
6924 statusBorderInfo[statusBorderItem.tags] = statusBorderItem;
6925 }
6926
6927 for (i = 0; i < sbsl; i++) {
6928 statusBorderItem = status_borders[i];
6929
6930 if (single_color_borders) {
6931 if (statusBorderItem.is_enabled)
6932 activeStatusStyles = '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}' + activeStatusStyles;
6933 else
6934 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: transparent !important;}'; // Disable status border by resetting it to transparent.
6935 }
6936 else {
6937 if (statusBorderItem.is_enabled) {
6938 if (statusBorderItem.tags === "parent") {
6939 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.
6940
6941 if (statusBorderInfo.child.is_enabled)
6942 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.
6943 }
6944 else if (statusBorderItem.tags === "child")
6945 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.
6946 else {
6947 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.
6948
6949 if (statusBorderInfo.parent.is_enabled)
6950 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.
6951
6952 if (statusBorderInfo.child.is_enabled)
6953 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.
6954
6955 if (statusBorderInfo.child.is_enabled && statusBorderInfo.parent.is_enabled)
6956 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.
6957 }
6958 }
6959 else
6960 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: transparent !important;}'; // Disable status border by resetting it to transparent.
6961 }
6962 }
6963
6964 styles += activeStatusStyles;
6965 }
6966 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
6967 var defaultStatusBorders = bbb.options.status_borders;
6968
6969 for (i = defaultStatusBorders.length - 1; i >= 0; i--) {
6970 statusBorderItem = defaultStatusBorders[i];
6971
6972 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}';
6973 }
6974 }
6975
6976 if (custom_tag_borders) {
6977 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.
6978 '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.
6979
6980 for (i = 0; i < sbsl; i++) {
6981 statusBorderItem = status_borders[i];
6982
6983 if (statusBorderItem.is_enabled)
6984 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.
6985 '.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.
6986 }
6987 }
6988
6989 // Overlay setup.
6990 styles += 'article.post-preview:before, div.post.post-preview div.preview:before {content: none !important;}' + // Disable original Danbooru animated overlay.
6991 '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.
6992 '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.
6993 '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.
6994 '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.
6995 '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.
6996 '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.
6997
6998 for (i = 0; i < sbsl; i++) {
6999 statusBorderItem = status_borders[i];
7000
7001 if (statusBorderItem.is_enabled)
7002 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.
7003 }
7004
7005 // Thumbnail info.
7006 var thumbInfoStyle = "height: 18px; font-size: 14px; line-height: 18px; text-align: center;";
7007
7008 if (thumb_info !== "disabled")
7009 styles += '.bbb-thumb-info-parent.blacklisted.blacklisted-active .bbb-thumb-info {display: none;}';
7010
7011 if (thumb_info === "below")
7012 styles += '.bbb-thumb-info-parent .bbb-thumb-info {display: block;' + thumbInfoStyle + '}';
7013 else if (thumb_info === "hover") {
7014 styles += '.bbb-thumb-info-parent .bbb-thumb-info {display: none; position: relative; bottom: 18px; background-color: rgba(255, 255, 255, 0.9);' + thumbInfoStyle + '}' +
7015 '.bbb-thumb-info-parent:hover .bbb-thumb-info {display: block;}' +
7016 '#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.
7017 '.bbb-thumb-info-parent:hover .bbb-thumb-info.bbb-thumb-info-short {bottom: 0px;}'; // Short thumbnails get no overlapping.
7018 }
7019
7020 // Endless
7021 if (endless_default !== "disabled") {
7022 styles += 'div.paginator {padding: 3em 0px 0px;}' +
7023 '#bbb-endless-button-div {width: 100%; height: 0px; overflow: visible; clear: both; text-align: center;}' +
7024 '#bbb-endless-load-div, #bbb-endless-enable-div {display: none; position: absolute;}' +
7025 '#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;}';
7026
7027 if (endless_separator === "divider") {
7028 styles += '.bbb-endless-page {display: block; clear: both;}' +
7029 '.bbb-endless-divider {display: block; border: 1px solid #CCCCCC; height: 0px; margin: 15px 0px; width: 100%; float: left;}' +
7030 '.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;}';
7031 }
7032 else if (endless_separator === "marker") {
7033 styles += '.bbb-endless-page {display: inline;}' +
7034 '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;}' +
7035 '.bbb-endless-marker {display: inline-block; border: 1px solid #CCCCCC; height: 148px; width: 148px; line-height: 148px; text-align: center; margin-top: ' + totalBorderWidth + 'px;}' +
7036 '.bbb-endless-marker-link {display: inline-block; font-size: 14px; font-weight: bold; line-height: 14px; vertical-align: middle; color: #CCCCCC;}';
7037 }
7038 else if (endless_separator === "none")
7039 styles += '.bbb-endless-page {display: inline;}';
7040 }
7041
7042 // Hide sidebar.
7043 if (autohide_sidebar) {
7044 styles += 'div#page {margin: 0px 10px 0px 20px !important;}' +
7045 '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;}' +
7046 '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;}' +
7047 'section#content {margin-left: 0px !important;}' +
7048 '.ui-autocomplete {z-index: 2002 !important;}';
7049 }
7050
7051 // Collapse sidebar sections.
7052 if (collapse_sidebar) {
7053 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.
7054 '#sidebar h1, #sidebar h2 {display: inline-block !important;}'; // Inline-block is possible here due to not using display in the previous rule.
7055 }
7056
7057 // Additional blacklist bars.
7058 if (blacklist_add_bars) {
7059 styles += '#blacklist-box.bbb-blacklist-box {margin-bottom: 1em;}' +
7060 '#blacklist-box.bbb-blacklist-box ul {display: inline;}' +
7061 '#blacklist-box.bbb-blacklist-box li {display: inline; margin-right: 1em;}' +
7062 '#blacklist-box.bbb-blacklist-box li a, #blacklist-box.bbb-blacklist-box li span.link {color: #0073FF; cursor: pointer;}' +
7063 '#blacklist-box.bbb-blacklist-box li span {color: #AAAAAA;}';
7064 }
7065
7066 // Blacklist thumbnail display.
7067 if (blacklist_post_display === "removed") {
7068 styles += 'article.post-preview.blacklisted {display: inline-block !important;}' +
7069 'article.post-preview.blacklisted.blacklisted-active {display: none !important;}' +
7070 'div.post.post-preview.blacklisted {display: block !important;}' + // Comment listing override.
7071 'div.post.post-preview.blacklisted.blacklisted-active {display: none !important;}';
7072 }
7073 else if (blacklist_post_display === "hidden") {
7074 styles += 'article.post-preview.blacklisted.blacklisted-active {display: inline-block !important;}' +
7075 'div.post.post-preview.blacklisted {display: block !important;}' + // Comments.
7076 '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;}';
7077 }
7078 else if (blacklist_post_display === "replaced") {
7079 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;}' +
7080 '#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.
7081 '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.
7082 '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.
7083 '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;}' +
7084 'div.post.post-preview.blacklisted {display: block !important;}' +
7085 'div.post.post-preview.blacklisted.blacklisted-active {display: block !important;}';
7086 }
7087
7088 // Blacklist marking.
7089 if (blacklist_thumb_mark === "icon") {
7090 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.
7091 }
7092 else if (blacklist_thumb_mark === "highlight") {
7093 styles += 'article.post-preview.blacklisted, div.post.post-preview.blacklisted div.preview {background-color: ' + blacklist_highlight_color + ' !important;}' +
7094 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active div.preview {background-color: transparent !important;}' +
7095 'article.post-preview.blacklisted.blacklisted-active.current-post {background-color: rgba(0, 0, 0, 0.1) !important}';
7096 }
7097
7098 // Blacklist post controls.
7099 if (blacklist_thumb_controls) {
7100 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;}' +
7101 '#bbb-blacklist-tip * {font-size: 12px; line-height: 14px;}' +
7102 '#bbb-blacklist-tip .blacklisted-active {text-decoration: line-through; font-weight: normal;}' +
7103 '#bbb-blacklist-tip ul {list-style: outside disc none; margin-top: 0px; margin-bottom: 0px; margin-left: 15px;}' +
7104 '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;}' +
7105 'article.post-preview.blacklisted.blacklisted-active a, div.post.post-preview.blacklisted.blacklisted-active div.preview a {cursor: pointer !important;}' +
7106 'article.post-preview.blacklisted, div.post.post-preview.blacklisted div.preview {position: relative !important;}' +
7107 '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;}' +
7108 '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;}' +
7109 'article.post-preview.blacklisted .bbb-close-circle, div.post.post-preview.blacklisted div.preview .bbb-close-circle {display: none;}';
7110 }
7111
7112 // Move save search to the sidebar.
7113 if (move_save_search) {
7114 styles += '.bbb-saved-search-item #saved-searches-nav, .bbb-saved-search-item #saved-searches-nav * {background-color: transparent; color: #0073FF; display: inline; font-family: Verdana,Helvetica,sans-serif; line-height: 1.25em; padding: 0px; margin: 0px; border: none;}' +
7115 '.bbb-saved-search-item #saved-searches-nav input:hover, .bbb-saved-search-item #saved-searches-nav button:hover {color: #80b9ff;}' +
7116 '.bbb-saved-search-item #saved-searches-nav input:focus, .bbb-saved-search-item #saved-searches-nav button:focus {outline: thin dotted;}';
7117 }
7118
7119 // Quick search styles.
7120 if (quick_search !== "disabled") {
7121 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;}' +
7122 '#bbb-quick-search-form {display: none;}' +
7123 '.bbb-quick-search-show #bbb-quick-search-form {display: inline;}' +
7124 '#bbb-quick-search-status, #bbb-quick-search-pin {border: none; width: 16px; height: 16px; background-color: transparent; background-repeat: no-repeat; background-color: transparent; background-image: url(\'/images/ui-icons_222222_256x240.png\');}' +
7125 '#bbb-quick-search-status {background-position: -160px -112px;}' + // Magnifying glass.
7126 '.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.
7127 '#bbb-quick-search-pin {background-position: -128px -145px;}' + // Horizontal pin.
7128 '.bbb-quick-search-pinned #bbb-quick-search-pin, .bbb-quick-search-active.bbb-quick-search-pinned #bbb-quick-search-status {background-position: -145px -145px;}' + // Vertical pin.
7129 '#bbb-quick-search.bbb-quick-search-active {background-color: #DDDDDD;}' +
7130 '#bbb-quick-search.bbb-quick-search-active.bbb-quick-search-show {background-color: #FFFFFF;}' +
7131 '#bbb-quick-search-pin:focus, #bbb-quick-search-pin:hover {background-color: #CCCCCC;}' +
7132 '#news-updates {padding-right: 25px !important;}';
7133
7134 if (quick_search.indexOf("remove") > -1)
7135 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;}';
7136 else if (quick_search.indexOf("fade") > -1)
7137 styles += 'article.post-preview.bbb-quick-search-filtered {opacity: 0.1;}';
7138 }
7139
7140 if (resize_link_style === "minimal")
7141 styles += '.bbb-resize-link {display: inline-block; text-align: center; margin-right: 2px; font-size: 87.5%;}';
7142
7143 if (search_add === "remove")
7144 styles += '.search-inc-tag, .search-exl-tag {display: none !important;}';
7145
7146 if (direct_downloads)
7147 styles += '.bbb-ddl {display: none !important;}';
7148
7149 if (post_tag_scrollbars)
7150 styles += '#tag-list ul {max-height: ' + post_tag_scrollbars + 'px !important; overflow-y: auto !important; font-size: 87.5% !important;}';
7151
7152 if (search_tag_scrollbars)
7153 styles += '#tag-box ul {max-height: ' + search_tag_scrollbars + 'px !important; overflow-y: auto !important; font-size: 87.5% !important; margin-right: 2px !important;}';
7154
7155 if (hide_tos_notice && document.getElementById("tos-notice")) {
7156 styles += '#tos-notice {display: none !important;}';
7157
7158 if (manage_cookies)
7159 createCookie("accepted_tos", 1, 365);
7160 }
7161
7162 if (hide_sign_up_notice && document.getElementById("sign-up-notice")) {
7163 styles += '#sign-up-notice {display: none !important;}';
7164
7165 if (manage_cookies)
7166 createCookie("hide_sign_up_notice", 1, 7);
7167 }
7168
7169 if (hide_upgrade_notice && document.getElementById("upgrade-account-notice")) {
7170 styles += '#upgrade-account-notice {display: none !important;}';
7171
7172 if (manage_cookies)
7173 createCookie("hide_upgrade_account_notice", 1, 7);
7174 }
7175
7176 if (hide_ban_notice)
7177 styles += '#ban-notice {display: none !important;}';
7178
7179 if (hide_comment_notice) {
7180 var commentGuide;
7181
7182 if (gLoc === "post") {
7183 commentGuide = document.evaluate('//section[@id="comments"]/h2/a[contains(@href,"/wiki_pages")]/..', document, null, 9, null).singleNodeValue;
7184
7185 if (commentGuide && commentGuide.textContent === "Before commenting, read the how to comment guide.")
7186 commentGuide.style.display = "none";
7187 }
7188 else if (gLoc === "comments") {
7189 commentGuide = document.evaluate('//div[@id="a-index"]/div/h2/a[contains(@href,"/wiki_pages")]/..', document, null, 9, null).singleNodeValue;
7190
7191 if (commentGuide && commentGuide.textContent === "Before commenting, read the how to comment guide.")
7192 commentGuide.style.display = "none";
7193 }
7194 }
7195
7196 if (hide_tag_notice && gLoc === "post") {
7197 var tagGuide = document.evaluate('//section[@id="edit"]/div/p/a[contains(@href,"/howto:tag")]/..', document, null, 9, null).singleNodeValue;
7198
7199 if (tagGuide && tagGuide.textContent === "Before editing, read the how to tag guide.")
7200 tagGuide.style.display = "none";
7201 }
7202
7203 if (hide_upload_notice && gLoc === "upload")
7204 styles += '#upload-guide-notice {display: none !important;}';
7205
7206 if (hide_pool_notice && gLoc === "new_pool") {
7207 var poolGuide = document.evaluate('//div[@id="c-new"]/p/a[contains(@href,"/howto:pools")]/..', document, null, 9, null).singleNodeValue;
7208
7209 if (poolGuide && poolGuide.textContent === "Before creating a pool, read the pool guidelines.")
7210 poolGuide.style.display = "none";
7211 }
7212
7213 customStyles.innerHTML = styles;
7214 document.getElementsByTagName("head")[0].appendChild(customStyles);
7215 }
7216
7217 function pageCounter() {
7218 // Set up the page counter.
7219 var pageEl = document.getElementById("page");
7220 var paginator = getPaginator();
7221
7222 if (!page_counter || !paginator || !pageEl)
7223 return;
7224
7225 var numString = "";
7226 var lastNumString;
7227
7228 // Provide page number info if available.
7229 if (/\d/.test(paginator.textContent)) {
7230 var activePage = paginator.getElementsByTagName("span")[0];
7231 var pageItems = paginator.getElementsByTagName("li");
7232 var numPageItems = pageItems.length;
7233 var lastPageItem = pageItems[numPageItems - 1];
7234 var activeNum = activePage.textContent.bbbSpaceClean();
7235 var lastNum;
7236
7237 if (activePage.parentNode === lastPageItem) // Last/only page case.
7238 lastNum = activeNum;
7239 else { // In all other cases, there should always be a next page button and at least two other page items (1-X).
7240 lastNum = lastPageItem.previousElementSibling.textContent.bbbSpaceClean();
7241
7242 if (!bbbIsNum(lastNum)) // Too many pages for the current user to view.
7243 lastNum = "";
7244 }
7245
7246 lastNumString = (lastNum ? " of " + lastNum : "");
7247 numString = 'Page ' + activeNum + '<span id="bbb-page-counter-last">' + lastNumString + '</span> | ';
7248 }
7249
7250 var pageNav = bbb.el.pageCounter;
7251 var pageInput = bbb.el.pageCounterInput;
7252
7253 if (!pageNav) { // Create the page nav.
7254 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>';
7255
7256 pageNav = bbb.el.pageCounter = document.createElement("div");
7257 pageNav.innerHTML = pageNavString;
7258
7259 pageInput = bbb.el.pageCounterInput = getId("bbb-page-counter-input", pageNav);
7260
7261 getId("bbb-page-counter-form", pageNav).addEventListener("submit", function(event) {
7262 var value = pageInput.value.bbbSpaceClean();
7263
7264 if (value !== "")
7265 location.href = updateURLQuery(location.href, {page:value});
7266
7267 event.preventDefault();
7268 }, false);
7269
7270 if (numString)
7271 paginator.bbbWatchNodes(pageCounter);
7272
7273 pageEl.insertBefore(pageNav, pageEl.firstElementChild);
7274 }
7275 else // Update the last page in the page nav.
7276 document.getElementById("bbb-page-counter-last").innerHTML = lastNumString;
7277 }
7278
7279 function quickSearch() {
7280 // Set up quick search.
7281 removeInheritedStorage("bbb_quick_search");
7282
7283 if (quick_search === "disabled" || (gLoc !== "search" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "pool" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorite_group"))
7284 return;
7285
7286 var allowAutocomplete = (getMeta("enable-auto-complete") === "true");
7287
7288 // Create the quick search.
7289 var searchDiv = bbb.el.quickSearchDiv = document.createElement("div");
7290 searchDiv.id = "bbb-quick-search";
7291 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-submit" type="submit" value="Go"></form>';
7292
7293 var searchForm = bbb.el.quickSearchForm = getId("bbb-quick-search-form", searchDiv);
7294 var searchInput = bbb.el.quickSearchInput = getId("bbb-quick-search-input", searchDiv);
7295 var searchPin = bbb.el.quickSearchPin = getId("bbb-quick-search-pin", searchDiv);
7296 var searchSubmit = bbb.el.quickSearchSubmit = getId("bbb-quick-search-submit", searchDiv);
7297 var searchStatus = bbb.el.quickSearchStatus = getId("bbb-quick-search-status", searchDiv);
7298
7299 // Make the submit event search posts or reset the search.
7300 searchForm.addEventListener("submit", function(event) {
7301 var oldValue = bbb.quick_search.bbbSpaceClean();
7302 var curValue = searchInput.value.bbbSpaceClean();
7303
7304 if (curValue === "" || curValue === oldValue)
7305 quickSearchReset();
7306 else {
7307 bbb.quick_search = bbb.el.quickSearchInput.value;
7308
7309 if (searchDiv.bbbHasClass("bbb-quick-search-pinned"))
7310 sessionStorage.bbbSetItem("bbb_quick_search", bbb.quick_search);
7311 else if (quick_search.indexOf("pinned") > -1)
7312 quickSearchPinEnable();
7313
7314 quickSearchTest();
7315 }
7316
7317 // Make autocomplete close without getting too tricky.
7318 searchSubmit.focus();
7319 delayMe(function() { searchInput.focus(); }); // Delay this so the blur event has time to register properly.
7320
7321 event.preventDefault();
7322 }, false);
7323
7324 // Hide the search div if the new focus isn't one of the inputs.
7325 searchDiv.addEventListener("blur", function(event) {
7326 var target = event.target;
7327
7328 delayMe(function() {
7329 var active = document.activeElement;
7330
7331 if (active === target || (active !== searchInput && active !== searchSubmit && active !== searchStatus && active !== searchPin))
7332 searchDiv.bbbRemoveClass("bbb-quick-search-show");
7333 });
7334 }, true);
7335
7336 // If a mouse click misses an input within the quick search div, cancel it so the quick search doesn't minimize.
7337 searchDiv.addEventListener("mousedown", function(event) {
7338 var target = event.target;
7339
7340 if (target === searchDiv || target === searchForm)
7341 event.preventDefault();
7342 }, false);
7343
7344 // Hide the search div if the escape key is pressed while using it and autocomplete isn't open.
7345 searchDiv.addEventListener("keydown", function(event) {
7346 if (event.keyCode === 27) {
7347 var jQueryMenu = (searchInput.bbbHasClass("ui-autocomplete-input") ? $("#bbb-quick-search-input").autocomplete("widget")[0] : undefined);
7348
7349 if (jQueryMenu && jQueryMenu.style.display !== "none")
7350 return;
7351
7352 document.activeElement.blur();
7353 event.preventDefault();
7354 }
7355 }, true);
7356
7357 // 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.
7358 searchStatus.addEventListener("click", function(event) {
7359 if (event.button === 0) {
7360 if (event.shiftKey)
7361 quickSearchPinToggle();
7362 else if (!searchDiv.bbbHasClass("bbb-quick-search-show"))
7363 quickSearchOpen();
7364 else
7365 searchDiv.bbbRemoveClass("bbb-quick-search-show");
7366 }
7367
7368 event.preventDefault();
7369 }, false);
7370
7371 // Reset via a right click on the main status icon.
7372 searchStatus.addEventListener("mouseup", function(event) {
7373 if (event.button === 2 && searchDiv.bbbHasClass("bbb-quick-search-active"))
7374 quickSearchReset();
7375
7376 event.preventDefault();
7377 }, false);
7378
7379 // Stop the context menu on the status icon.
7380 searchStatus.addEventListener("contextmenu", disableEvent, false);
7381
7382 // Make the pin input toggle the pinned status.
7383 searchPin.addEventListener("click", function(event) {
7384 if (event.button === 0)
7385 quickSearchPinToggle();
7386 }, false);
7387
7388 // Watch the input value and adjust the quick search as needed.
7389 searchInput.addEventListener("input", quickSearchCheck, false);
7390 searchInput.addEventListener("keyup", quickSearchCheck, false);
7391 searchInput.addEventListener("cut", quickSearchCheck, false);
7392 searchInput.addEventListener("paste", quickSearchCheck, false);
7393 searchInput.addEventListener("change", quickSearchCheck, false);
7394
7395 document.body.insertBefore(searchDiv, document.body.firstElementChild);
7396
7397 // Force the submit button to retain its width.
7398 searchDiv.bbbAddClass("bbb-quick-search-show");
7399 searchSubmit.style.width = searchSubmit.offsetWidth + "px";
7400 searchDiv.bbbRemoveClass("bbb-quick-search-show");
7401
7402 // Take of a copy of Danbooru's autocomplete and modify it for the search.
7403 if (allowAutocomplete && Danbooru.Autocomplete && Danbooru.Autocomplete.initialize_tag_autocomplete) {
7404 try {
7405 var autoComplete = Danbooru.Autocomplete.initialize_tag_autocomplete.toString().match(/\{([\s\S]*)\}/)[1];
7406 var searchAutoComplete = autoComplete.replace(/(,)#tags|#tags(,)/, "$1#tags,#bbb-quick-search-input$2"); // /\$\([\s\S]*?#tags[\s\S]*?\)([\s\S]*?)\$\([\s\S]*?#artist_name[\s\S]*?\)/, '$("#bbb-quick-search-input")$1$()'
7407 var autoInit = new Function(searchAutoComplete);
7408
7409 autoInit();
7410
7411 // Counter normal autocomplete getting turned back on after submitting an input.
7412 document.body.addEventListener("focus", function(event) {
7413 var target = event.target;
7414
7415 if (target.bbbHasClass("ui-autocomplete-input"))
7416 target.setAttribute("autocomplete", "off");
7417 }, true);
7418
7419 // Make autocomplete fixed like the quick search.
7420 $(searchInput).autocomplete("widget").css("position", "fixed");
7421 }
7422 catch (error) {
7423 bbbNotice("Unexpected error while trying to initialize autocomplete for the quick search. (Error: " + error.message + ")", -1);
7424 }
7425 }
7426
7427 // Checked if the quick search has been pinned for this session.
7428 var pinnedSearch = sessionStorage.getItem("bbb_quick_search");
7429
7430 if (pinnedSearch) {
7431 bbb.quick_search = pinnedSearch;
7432 searchInput.value = pinnedSearch;
7433 searchDiv.bbbAddClass("bbb-quick-search-pinned");
7434 quickSearchTest();
7435 }
7436 }
7437
7438 function quickSearchCheck() {
7439 // Check the input value and adjust the submit button appearance accordingly.
7440 var input = bbb.el.quickSearchInput;
7441 var submit = bbb.el.quickSearchSubmit;
7442 var oldValue = bbb.quick_search.bbbSpaceClean();
7443 var curValue = input.value.bbbSpaceClean();
7444
7445 if (oldValue === curValue && curValue !== "")
7446 submit.value = "X";
7447 else
7448 submit.value = "Go";
7449 }
7450
7451 function quickSearchReset() {
7452 // Completely reset the quick search.
7453 var filtered = document.getElementsByClassName("bbb-quick-search-filtered");
7454
7455 bbb.quick_search = "";
7456 bbb.el.quickSearchInput.value = "";
7457 bbb.el.quickSearchSubmit.value = "Go";
7458 bbb.el.quickSearchStatus.title = "";
7459 sessionStorage.removeItem("bbb_quick_search");
7460 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-active bbb-quick-search-pinned");
7461
7462 while (filtered[0])
7463 filtered[0].bbbRemoveClass("bbb-quick-search-filtered");
7464 }
7465
7466 function quickSearchTest(target) {
7467 // Test posts to see if they match the search.
7468 var value = bbb.quick_search.bbbSpaceClean();
7469
7470 if (value === "")
7471 return;
7472
7473 var posts = getPosts(target);
7474 var search = createSearch(value);
7475
7476 bbb.el.quickSearchSubmit.value = "X";
7477 bbb.el.quickSearchStatus.title = value;
7478 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-active");
7479
7480 for (var i = 0, il = posts.length; i < il; i++) {
7481 var post = posts[i];
7482
7483 if (!thumbSearchMatch(post, search))
7484 post.bbbAddClass("bbb-quick-search-filtered");
7485 else
7486 post.bbbRemoveClass("bbb-quick-search-filtered");
7487 }
7488 }
7489
7490 function quickSearchOpen() {
7491 // Open the quick search div and place focus on the input.
7492 var searchInput = bbb.el.quickSearchInput;
7493
7494 searchInput.value = bbb.quick_search;
7495 quickSearchCheck();
7496 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-show");
7497 searchInput.focus();
7498 }
7499
7500 function quickSearchPinToggle() {
7501 // Toggle the quick search between pinned and not pinned for the session.
7502 var searchDiv = bbb.el.quickSearchDiv;
7503
7504 if (searchDiv.bbbHasClass("bbb-quick-search-show") || searchDiv.bbbHasClass("bbb-quick-search-active")) {
7505 if (!searchDiv.bbbHasClass("bbb-quick-search-pinned"))
7506 quickSearchPinEnable();
7507 else
7508 quickSearchPinDisable();
7509 }
7510 }
7511
7512 function quickSearchPinEnable() {
7513 // Enable the quick search pin.
7514 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-pinned");
7515
7516 if (bbb.quick_search)
7517 sessionStorage.bbbSetItem("bbb_quick_search", bbb.quick_search);
7518 }
7519
7520 function quickSearchPinDisable() {
7521 // Disable the quick search pin.
7522 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-pinned");
7523 sessionStorage.removeItem("bbb_quick_search");
7524 }
7525
7526 function moveSaveSearch() {
7527 // Move the "save search" div into the sidebar related section and style it as a link.
7528 var saveSearchDiv = document.getElementById("saved-searches-nav");
7529 var relatedSection = document.getElementById("related-box");
7530
7531 if (!move_save_search || !saveSearchDiv || !relatedSection)
7532 return;
7533
7534 saveSearchDiv.parentNode.removeChild(saveSearchDiv);
7535
7536 var relatedSectionMenu = relatedSection.getElementsByTagName("ul")[0];
7537
7538 var saveSearchItem = document.createElement("li");
7539 saveSearchItem.className = "bbb-saved-search-item";
7540 saveSearchItem.appendChild(saveSearchDiv);
7541
7542 relatedSectionMenu.insertBefore(saveSearchItem, relatedSectionMenu.firstElementChild);
7543 }
7544
7545 function commentScoreInit() {
7546 // Set up the initial comment scores and get ready to handle new comments.
7547 if (!comment_score || (gLoc !== "comments" && gLoc !== "comment_search" && gLoc !== "comment" && gLoc !== "post"))
7548 return;
7549
7550 var paginator = getPaginator();
7551 var watchedNode = (paginator ? paginator.parentNode : document.body);
7552
7553 commentScore();
7554 watchedNode.bbbWatchNodes(commentScore);
7555 }
7556
7557 function commentScore() {
7558 // Add score links to comments that link directly to that comment.
7559 var comments = document.getElementsByClassName("comment");
7560 var scoredComments = document.getElementsByClassName("bbb-comment-score");
7561
7562 // If all the comments are scored, just stop.
7563 if (comments.length === scoredComments.length)
7564 return;
7565
7566 for (var i = 0, il = comments.length; i < il; i++) {
7567 var comment = comments[i];
7568
7569 // Skip if the comment is already scored.
7570 if (comment.getElementsByClassName("bbb-comment-score")[0])
7571 continue;
7572
7573 var score = comment.getAttribute("data-score");
7574 var commentId = comment.getAttribute("data-comment-id");
7575 var content = comment.getElementsByClassName("content")[0];
7576 var menu = (content ? content.getElementsByTagName("menu")[0] : undefined);
7577
7578 if (content && !menu) {
7579 menu = document.createElement("menu");
7580 content.appendChild(menu);
7581 }
7582
7583 var menuItems = menu.getElementsByTagName("li");
7584 var listingItemSibling = menuItems[1];
7585
7586 for (var j = 0, jl = menuItems.length; j < jl; j++) {
7587 var menuItem = menuItems[j];
7588 var nextItem = menuItems[j + 1];
7589
7590 if (menuItem.textContent.indexOf("Reply") > -1) {
7591 if (nextItem)
7592 listingItemSibling = nextItem;
7593 else
7594 listingItemSibling = undefined;
7595 }
7596 }
7597
7598 var scoreItem = document.createElement("li");
7599 scoreItem.className = "bbb-comment-score";
7600
7601 var scoreLink = document.createElement("a");
7602 scoreLink.innerHTML = "Score: " + score;
7603 scoreLink.href = "/comments/" + commentId;
7604 scoreItem.appendChild(scoreLink);
7605
7606 if (listingItemSibling)
7607 menu.insertBefore(scoreItem, listingItemSibling);
7608 else
7609 menu.appendChild(scoreItem);
7610 }
7611 }
7612
7613 function thumbInfo(target) {
7614 // Add score, favorite count, and rating info to thumbnails.
7615 var posts = getPosts(target);
7616
7617 if (thumb_info === "disabled" || !posts[0])
7618 return;
7619
7620 for (var i = 0, il = posts.length; i < il; i++) {
7621 var post = posts[i];
7622
7623 // Skip thumbnails that already have the info added.
7624 if (post.getElementsByClassName("bbb-thumb-info")[0])
7625 continue;
7626
7627 var score = Number(post.getAttribute("data-score"));
7628 var favCount = post.getAttribute("data-fav-count");
7629 var rating = post.getAttribute("data-rating").toUpperCase();
7630 var width = Number(post.getAttribute("data-width"));
7631 var height = Number(post.getAttribute("data-height"));
7632 var tooShort = (150 / width * height < 30); // Short thumbnails will need the info div position adjusted.
7633
7634 if (gLoc === "comments") { // Add favorites info to the existing info in the comments listing.
7635 var firstInfo = post.getElementsByClassName("info")[0];
7636 var infoParent = (firstInfo ? firstInfo.parentNode : undefined);
7637
7638 if (infoParent) {
7639 var favSpan = document.createElement("span");
7640 favSpan.className = "info bbb-thumb-info";
7641 favSpan.innerHTML = '<strong>Favorites</strong> ' + favCount;
7642 infoParent.appendChild(favSpan);
7643 }
7644 }
7645 else { // Add extra information inside of the thumbnail's parent element.
7646 var thumbImg = post.getElementsByTagName("img")[0];
7647
7648 // Don't add the info if there isn't a thumbnail.
7649 if (!thumbImg)
7650 continue;
7651
7652 var thumbEl = post.getElementsByClassName("preview")[0] || post;
7653 thumbEl.bbbAddClass("bbb-thumb-info-parent");
7654
7655 var postLink = thumbEl.getElementsByTagName("a")[0];
7656 var before = (postLink ? postLink.nextElementSibling : undefined);
7657
7658 score = (score < 0 ? '<span style="color: #CC0000;">' + score + '</span>' : score);
7659
7660 var infoDiv = document.createElement("div");
7661 infoDiv.className = "bbb-thumb-info" + (tooShort ? " bbb-thumb-info-short" : "");
7662 infoDiv.innerHTML = "★" + score + " ♥" + favCount + (location.host.indexOf("safebooru") < 0 ? " " + rating : "");
7663
7664 if (before)
7665 thumbEl.insertBefore(infoDiv, before);
7666 else
7667 thumbEl.appendChild(infoDiv);
7668 }
7669 }
7670 }
7671
7672 function postLinkNewWindow() {
7673 // Make thumbnail clicks open in a new tab/window.
7674 if (post_link_new_window === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorite_group"))
7675 return;
7676
7677 document.addEventListener("click", function(event) {
7678 var bypass = (event.shiftKey && event.ctrlKey);
7679 var modeSection = document.getElementById("mode-box");
7680 var danbMode = getCookie().mode || "view";
7681
7682 if (event.button !== 0 || event.altKey || !bypass && (event.shiftKey || event.ctrlKey) || (modeSection && danbMode !== "view"))
7683 return;
7684
7685 var runEndless = (post_link_new_window.indexOf("endless") > -1);
7686 var runNormal = (post_link_new_window.indexOf("normal") > -1);
7687
7688 if ((bbb.endless.enabled && !runEndless) || (!bbb.endless.enabled && !runNormal))
7689 return;
7690
7691 var target = event.target;
7692 var targetTag = target.tagName;
7693 var url;
7694
7695 if (targetTag === "IMG" && target.parentNode)
7696 url = target.parentNode.href;
7697 else if (targetTag === "A" && target.bbbHasClass("bbb-post-link", "bbb-thumb-link"))
7698 url = target.href;
7699
7700 if (url && /\/posts\/\d+/.test(url)) {
7701 if (bypass)
7702 location.href = url;
7703 else
7704 window.open(url);
7705
7706 event.preventDefault();
7707 }
7708 }, false);
7709 }
7710
7711 function formatTip(event, el, content, x, y) {
7712 // Position + resize the tip and display it.
7713 var tip = el;
7714 var windowX = event.clientX;
7715 var windowY = event.clientY;
7716 var topOffset = 0;
7717 var leftOffset = 0;
7718
7719 if (typeof(content) === "string")
7720 tip.innerHTML = content;
7721 else {
7722 tip.innerHTML = "";
7723 tip.appendChild(content);
7724 }
7725
7726 tip.style.visibility = "hidden";
7727 tip.style.display = "block";
7728
7729 // Resize the tip to minimize blank space.
7730 var origHeight = tip.clientHeight;
7731 var padding = tip.bbbGetPadding();
7732 var paddingWidth = padding.width;
7733
7734 while (origHeight >= tip.clientHeight && tip.clientWidth > 15)
7735 tip.style.width = tip.clientWidth - paddingWidth - 2 + "px";
7736
7737 tip.style.width = tip.clientWidth - paddingWidth + 2 + "px";
7738
7739 if (tip.scrollWidth > tip.clientWidth)
7740 tip.style.width = "auto";
7741
7742 // Don't allow the tip to go above the top of the window.
7743 if (windowY - tip.offsetHeight < 5)
7744 topOffset = windowY - tip.offsetHeight - 5;
7745
7746 // Don't allow the tip to go beyond the left edge of the window.
7747 if (windowX - tip.offsetWidth < 5)
7748 leftOffset = tip.offsetWidth + 1;
7749
7750 tip.style.left = x - tip.offsetWidth + leftOffset + "px";
7751 tip.style.top = y - tip.offsetHeight - topOffset + "px";
7752 tip.style.visibility = "visible";
7753 }
7754
7755 function bbbHotkeys() {
7756 // Handle keydown events not taking place in text inputs and check if they're a hotkey.
7757 document.addEventListener("keydown", function(event) {
7758 var active = document.activeElement;
7759 var activeTag = active.tagName;
7760 var activeType = active.type;
7761
7762 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
7763 return;
7764
7765 var loc = (gLoc === "post" ? "post" : "other");
7766 var hotkeyCode = createHotkeyCode(event);
7767 var hotkey = bbb.hotkeys[loc][hotkeyCode];
7768
7769 if (hotkey) {
7770 var customHandler = hotkey.custom_handler;
7771 customHandler = (typeof(customHandler) !== "boolean" || customHandler !== true ? false : true);
7772
7773 hotkey.func(event); // The event object will always be the first argument passed to the provided function (previously declared or anonymous).
7774
7775 if (!customHandler) {
7776 event.preventDefault();
7777 event.stopPropagation();
7778 }
7779 }
7780 }, true);
7781 }
7782
7783 function createHotkeyCode(event) {
7784 // Take a keyboard event and create a code for its key combination.
7785 // Examples: s49 = Shift + "1", a50 = Alt + "2", cs51 = Control + Shift + "3"
7786 // Alt, control, meta, and shift abbreviations should be alphabetical. Keycode numbers: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
7787 var hotkeycode = "";
7788
7789 if (event.altKey)
7790 hotkeycode += "a";
7791
7792 if (event.ctrlKey)
7793 hotkeycode += "c";
7794
7795 if (event.metaKey)
7796 hotkeycode += "m";
7797
7798 if (event.shiftKey)
7799 hotkeycode += "s";
7800
7801 if (event.keyCode)
7802 hotkeycode += event.keyCode;
7803
7804 return hotkeycode || undefined;
7805 }
7806
7807 function createHotkey(hotkeyCode, func, propObject) {
7808 // Create hotkeys or override Danbooru's existing ones. Creating a hotkey for a hotkey that already exists will replace it.
7809 var loc = (gLoc === "post" ? "post" : "other");
7810 var hotkeyObject = {func: func};
7811
7812 if (propObject) {
7813 for (var i in propObject) {
7814 if (propObject.hasOwnProperty(i))
7815 hotkeyObject[i] = propObject[i];
7816 }
7817 }
7818
7819 bbb.hotkeys[loc][hotkeyCode] = hotkeyObject;
7820 }
7821
7822 function removeHotkey(hotkeyCode) {
7823 // Remove a hotkey.
7824 var loc = (gLoc === "post" ? "post" : "other");
7825
7826 delete bbb.hotkeys[loc][hotkeyCode];
7827 }
7828
7829 function resizeHotkey(event) {
7830 // Handle the resize post hotkeys and make sure they don't interfere with the favorite group dialog box hotkeys.
7831 var favGroup = document.querySelector("div[aria-describedby='add-to-favgroup-dialog']");
7832
7833 if (favGroup && favGroup.style.display !== "none")
7834 return;
7835
7836 var keyCode = event.keyCode;
7837 var mode;
7838
7839 switch (keyCode) {
7840 case 49:
7841 mode = "all";
7842 break;
7843 case 50:
7844 mode = "width";
7845 break;
7846 case 51:
7847 mode = "height";
7848 break;
7849 case 52:
7850 default:
7851 mode = "none";
7852 break;
7853 }
7854
7855 resizePost(mode);
7856 event.preventDefault();
7857 event.stopPropagation();
7858 }
7859
7860 function fixLimit(limit) {
7861 // Add the limit variable to link URLs that are not thumbnails.
7862 if (!thumbnail_count && limit === undefined)
7863 return;
7864
7865 var newLimit = (limit === undefined ? thumbnail_count : limit) || undefined;
7866 var page = document.getElementById("page");
7867 var header = document.getElementById("top");
7868 var searchParent = document.getElementById("search-box") || document.getElementById("a-intro");
7869 var links;
7870 var link;
7871 var linkHref;
7872 var i, il; // Loop variables.
7873
7874 if (page) {
7875 links = page.getElementsByTagName("a");
7876
7877 for (i = 0, il = links.length; i < il; i++) {
7878 link = links[i];
7879 linkHref = link.getAttribute("href"); // Use getAttribute so that we get the exact value. "link.href" adds in the domain.
7880
7881 if (linkHref && !/page=/.test(linkHref) && (linkHref.indexOf("/posts?") === 0 || linkHref.indexOf("/favorites?") === 0))
7882 link.href = updateURLQuery(linkHref, {limit: newLimit});
7883 }
7884 }
7885
7886 if (header) {
7887 links = header.getElementsByTagName("a");
7888
7889 for (i = 0, il = links.length; i < il; i++) {
7890 link = links[i];
7891 linkHref = link.getAttribute("href");
7892
7893 if (linkHref && (linkHref.indexOf("limit=") > -1 || linkHref.indexOf("/posts") === 0 || linkHref === "/" || linkHref === "/notes?group_by=post" || linkHref === "/favorites"))
7894 link.href = updateURLQuery(linkHref, {limit: newLimit});
7895 }
7896 }
7897
7898 // Fix the search.
7899 if (searchParent && (gLoc === "search" || gLoc === "post" || gLoc === "intro" || gLoc === "favorites")) {
7900 var search = searchParent.getElementsByTagName("form")[0];
7901
7902 if (search) {
7903 var limitInput = bbb.el.limitInput;
7904
7905 if (!limitInput) {
7906 limitInput = bbb.el.limitInput = document.createElement("input");
7907 limitInput.name = "limit";
7908 limitInput.value = newLimit;
7909 limitInput.type = "hidden";
7910 search.appendChild(limitInput);
7911
7912 // 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.
7913 search.action = "/posts";
7914
7915 // Remove the user's default limit if the user tries to specify a limit value in the tags.
7916 var tagsInput = document.getElementById("tags");
7917
7918 if (tagsInput) {
7919 search.addEventListener("submit", function() {
7920 if (/(?:^|\s)limit:/.test(tagsInput.value))
7921 search.removeChild(limitInput);
7922 else if (limitInput.parentNode !== search)
7923 search.appendChild(limitInput);
7924 }, false);
7925 }
7926 }
7927 else
7928 limitInput.value = newLimit || thumbnail_count_default;
7929 }
7930 }
7931 }
7932
7933 function getLimit(url) {
7934 // Retrieve the current specified limit value. The query limit overrides the search limit.
7935 var loc = danbLoc(url);
7936 var limit;
7937
7938 if (loc === "pool" || loc === "popular" || loc === "favorite_group")
7939 limit = thumbnail_count_default;
7940 else if (loc === "comments")
7941 limit = 5;
7942 else if (loc === "popular_view")
7943 limit = 101;
7944 else {
7945 var queryLimit = getQueryLimit(url);
7946 var searchLimit = getSearchLimit(url);
7947
7948 limit = (queryLimit !== undefined ? queryLimit : searchLimit);
7949 }
7950
7951 return limit;
7952 }
7953
7954 function getQueryLimit(url) {
7955 // Retrieve the limit from a URL's query portion. Always use the default for certain areas where the limit is not allowed.
7956 var queryLimit = getVar("limit", url);
7957
7958 if (queryLimit !== null && queryLimit !== undefined) { // Treat the limit as undefined when the limit parameter is declared with no value.
7959 queryLimit = decodeURIComponent(queryLimit);
7960
7961 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.
7962 return 0;
7963 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.
7964 return parseInt(queryLimit, 10);
7965 }
7966
7967 return undefined;
7968 }
7969
7970 function getSearchLimit(url) {
7971 // Retrieve the limit from the search/limit tag used in a search.
7972 var searchLimit = getTagVar("limit", url);
7973
7974 if (searchLimit !== undefined) {
7975 searchLimit = decodeURIComponent(searchLimit);
7976
7977 if (searchLimit === "") // No thumbnails show up when the limit is declared but left blank.
7978 return 0;
7979 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.
7980 return undefined;
7981 else
7982 return Number(searchLimit);
7983 }
7984
7985 return undefined;
7986 }
7987
7988 function arrowNav() {
7989 // Bind the arrow keys to Danbooru's page navigation.
7990 var paginator = getPaginator();
7991
7992 if (!arrow_nav || (!paginator && gLoc !== "popular")) // If the paginator exists, arrow navigation should be applicable.
7993 return;
7994
7995 // Create the hotkeys for the left and right arrows.
7996 createHotkey("37", function() { danbooruNav("prev"); });
7997 createHotkey("39", function() { danbooruNav("next"); });
7998 }
7999
8000 function danbooruNav(dir) {
8001 // Determine the correct Danbooru page function and use it.
8002 if (gLoc === "popular") {
8003 if (dir === "prev")
8004 Danbooru.PostPopular.nav_prev();
8005 else if (dir === "next")
8006 Danbooru.PostPopular.nav_next();
8007 }
8008 else {
8009 if (dir === "prev")
8010 Danbooru.Paginator.prev_page();
8011 else if (dir === "next")
8012 Danbooru.Paginator.next_page();
8013 }
8014 }
8015
8016 function autohideSidebar() {
8017 // Show the sidebar when it gets focus, hide it when it loses focus, and only allow select elements to retain focus.
8018 var sidebar = document.getElementById("sidebar");
8019
8020 if (!autohide_sidebar || !sidebar)
8021 return;
8022
8023 sidebar.addEventListener("click", function(event) {
8024 var target = event.target;
8025
8026 if (target.id !== "tags")
8027 target.blur();
8028 }, false);
8029 sidebar.addEventListener("focus", function() {
8030 sidebar.bbbAddClass("bbb-sidebar-show");
8031 }, true);
8032 sidebar.addEventListener("blur", function() {
8033 sidebar.bbbRemoveClass("bbb-sidebar-show");
8034 }, true);
8035 }
8036
8037 function fixedSidebar() {
8038 // Fix the scrollbar to the top/bottom of the window when it would normally scroll out of view.
8039 var sidebar = bbb.fixed_sidebar.sidebar = document.getElementById("sidebar");
8040 var content = bbb.fixed_sidebar.content = document.getElementById("content");
8041 var comments = document.getElementById("comments");
8042
8043 if (!fixed_sidebar || autohide_sidebar || !sidebar || !content || (gLoc === "post" && !comments))
8044 return;
8045
8046 var docRect = document.documentElement.getBoundingClientRect();
8047 var sidebarRect = sidebar.getBoundingClientRect();
8048 var sidebarTop = bbb.fixed_sidebar.top = sidebarRect.top - docRect.top;
8049 var sidebarLeft = bbb.fixed_sidebar.left = sidebarRect.left - docRect.left;
8050 var sidebarHeight = sidebarRect.height;
8051
8052 content.style.minHeight = sidebarHeight - 1 + "px";
8053 sidebar.style.overflow = "hidden"; // There are some cases where text overflows.
8054
8055 if (comments)
8056 comments.style.overflow = "auto"; // Force the contained float elements to affect the dimensions.
8057
8058 fixedSidebarCheck();
8059 document.body.bbbWatchNodes(fixedSidebarCheck);
8060 document.addEventListener("keyup", fixedSidebarCheck, false);
8061 document.addEventListener("click", fixedSidebarCheck, false);
8062 window.addEventListener("scroll", fixedSidebarCheck, false);
8063 window.addEventListener("resize", fixedSidebarCheck, false);
8064 }
8065
8066 function fixedSidebarCheck() {
8067 // Event handler for adjusting the sidebar position.
8068 var sidebar = bbb.fixed_sidebar.sidebar;
8069 var content = bbb.fixed_sidebar.content;
8070 var sidebarTop = bbb.fixed_sidebar.top;
8071 var sidebarLeft = bbb.fixed_sidebar.left;
8072 var verScrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
8073 var horScrolled = window.payeXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
8074 var sidebarHeight = sidebar.clientHeight; // Height can potentially change (blacklist update, etc.) so always recalculate it.
8075 var contentHeight = content.clientHeight;
8076 var viewHeight = document.documentElement.clientHeight;
8077 var sidebarBottom = sidebarTop + sidebarHeight;
8078 var contentBottom = sidebarTop + contentHeight;
8079 var viewportBottom = verScrolled + viewHeight;
8080
8081 if (sidebarHeight > contentHeight) // Don't fix to window if there's no space for it to scroll.
8082 sidebar.style.position = "static";
8083 else if (sidebarHeight < viewHeight) { // Fix to the top of the window if not too tall and far enough down.
8084 if (contentBottom < verScrolled + sidebarHeight) {
8085 sidebar.style.position = "absolute";
8086 sidebar.style.bottom = viewHeight - contentBottom + "px";
8087 sidebar.style.top = "auto";
8088 }
8089 else if (sidebarTop < verScrolled ) {
8090 sidebar.style.position = "fixed";
8091 sidebar.style.bottom = "auto";
8092 sidebar.style.top = "0px";
8093 }
8094 else
8095 sidebar.style.position = "static";
8096 }
8097 else { // If too tall, fix the sidebar bottom to the viewport bottom to avoid putting part of the sidebar permanently beyond reach.
8098 if (viewportBottom > contentBottom) {
8099 sidebar.style.position = "absolute";
8100 sidebar.style.bottom = viewHeight - contentBottom + "px";
8101 sidebar.style.top = "auto";
8102 }
8103 else if (sidebarTop > verScrolled || sidebarTop > viewportBottom - sidebarHeight)
8104 sidebar.style.position = "static";
8105 else if (sidebarBottom < viewportBottom) {
8106 sidebar.style.position = "fixed";
8107 sidebar.style.bottom = "0px";
8108 sidebar.style.top = "auto";
8109 }
8110 }
8111
8112 // Maintain horizontal position in the document.
8113 if (horScrolled && sidebar.style.position !== "absolute")
8114 sidebar.style.left = (sidebarLeft - horScrolled) + "px";
8115 else
8116 sidebar.style.left = sidebarLeft + "px";
8117 }
8118
8119 function fixedPaginator() {
8120 // Set up the fixed paginator.
8121 if (fixed_paginator === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "favorite_group"))
8122 return;
8123
8124 var paginator = getPaginator();
8125 var paginatorMenu = (paginator ? paginator.getElementsByTagName("menu")[0] : undefined);
8126 var paginatorLink = (paginatorMenu ? (paginatorMenu.getElementsByTagName("a")[0] || paginatorMenu.getElementsByTagName("span")[0]) : undefined);
8127
8128 if (!paginatorLink)
8129 return;
8130
8131 // Get all our measurements.
8132 var docRect = document.documentElement.getBoundingClientRect();
8133 var docWidth = docRect.width;
8134 var docBottom = docRect.bottom;
8135
8136 var paginatorRect = paginator.getBoundingClientRect();
8137 var paginatorBottom = paginatorRect.bottom;
8138 var paginatorLeft = paginatorRect.left;
8139 var paginatorRight = docWidth - paginatorRect.right;
8140 var paginatorHeight = paginatorRect.height;
8141
8142 var menuRect = paginatorMenu.getBoundingClientRect();
8143 var menuBottom = menuRect.bottom;
8144
8145 var linkRect = paginatorLink.getBoundingClientRect();
8146 var linkBottom = linkRect.bottom;
8147
8148 var paginatorMargAdjust = (paginatorLeft - paginatorRight) / 2;
8149 var menuBottomAdjust = linkBottom - menuBottom;
8150
8151 var paginatorSpacer = document.createElement("div"); // Prevents the document height form changing when the paginator is fixed to the bottom of the window.
8152 paginatorSpacer.id = "bbb-fixed-paginator-spacer";
8153
8154 var paginatorSibling = paginator.nextElementSibling;
8155
8156 if (paginatorSibling)
8157 paginator.parentNode.insertBefore(paginatorSpacer, paginatorSibling);
8158 else
8159 paginator.parentNode.appendChild(paginatorSpacer);
8160
8161 // 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.
8162 var style = document.createElement("style");
8163 style.type = "text/css";
8164 style.innerHTML = '.bbb-fixed-paginator div.paginator {position: fixed; padding: 0px; margin: 0px; bottom: 0px; left: 50%; margin-left: ' + paginatorMargAdjust + 'px;}' +
8165 '.bbb-fixed-paginator div.paginator menu {position: relative; left: -50%; padding: ' + menuBottomAdjust + 'px 0px; background-color: #FFFFFF;}' +
8166 '.bbb-fixed-paginator div.paginator menu li:first-child {padding-left: 0px;}' +
8167 '.bbb-fixed-paginator div.paginator menu li:first-child > * {margin-left: 0px;}' +
8168 '.bbb-fixed-paginator div.paginator menu li:last-child {padding-right: 0px;}' +
8169 '.bbb-fixed-paginator div.paginator menu li:last-child > * {margin-right: 0px;}' +
8170 '#bbb-fixed-paginator-spacer {display: none; height: ' + paginatorHeight + 'px; clear: both; width: 100%;}' +
8171 '.bbb-fixed-paginator #bbb-fixed-paginator-spacer {display: block;}';
8172
8173 if (fixed_paginator.indexOf("minimal") > -1) {
8174 style.innerHTML += '.bbb-fixed-paginator div.paginator menu {padding: 3px 0px;}' +
8175 '.bbb-fixed-paginator div.paginator menu li a, .bbb-fixed-paginator div.paginator menu li span {padding: 2px; margin: 0px 2px 0px 0px;}' +
8176 '.bbb-fixed-paginator div.paginator menu li {padding: 0px;}' +
8177 '.bbb-fixed-paginator div.paginator menu li a {border-color: #CCCCCC;}';
8178 }
8179
8180 document.getElementsByTagName("head")[0].appendChild(style);
8181
8182 bbb.fixed_paginator_space = docBottom - paginatorBottom - menuBottomAdjust; // Store the amount of space between the bottom of the page and the paginator.
8183
8184 document.body.bbbWatchNodes(fixedPaginatorCheck);
8185 document.addEventListener("keyup", fixedPaginatorCheck, false);
8186 document.addEventListener("click", fixedPaginatorCheck, false);
8187 window.addEventListener("scroll", fixedPaginatorCheck, false);
8188 window.addEventListener("resize", fixedPaginatorCheck, false);
8189
8190 fixedPaginatorCheck();
8191 }
8192
8193 function fixedPaginatorCheck() {
8194 // Check if the paginator needs to be in its default position or fixed to the window.
8195 if (!bbb.fixed_paginator_space)
8196 return;
8197
8198 var runEndless = (fixed_paginator.indexOf("endless") > -1);
8199 var runNormal = (fixed_paginator.indexOf("normal") > -1);
8200 var docHeight = document.documentElement.scrollHeight;
8201 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
8202 var viewHeight = document.documentElement.clientHeight;
8203
8204 if (viewHeight + scrolled < docHeight - bbb.fixed_paginator_space && ((runEndless && bbb.endless.enabled) || (runNormal && !bbb.endless.enabled)))
8205 document.body.bbbAddClass("bbb-fixed-paginator");
8206 else
8207 document.body.bbbRemoveClass("bbb-fixed-paginator");
8208 }
8209
8210 function collapseSidebar() {
8211 // Allow clicking on headers to collapse and expand their respective sections.
8212 var sidebar = document.getElementById("sidebar");
8213
8214 if (!collapse_sidebar || !sidebar)
8215 return;
8216
8217 var dataLoc = (gLoc === "post" ? "post" : "thumb");
8218 var data = collapse_sidebar_data[dataLoc];
8219 var tagTypes = ["h1", "h2"]; // Tags that will be allowed to toggle.
8220 var nameList = " ";
8221 var removedOld = false;
8222 var i, il; // Loop variables.
8223
8224 // Grab the desired tags and turn them into toggle elements for their section.
8225 for (i = 0, il = tagTypes.length; i < il; i++) {
8226 var tags = sidebar.getElementsByTagName(tagTypes[i]);
8227
8228 for (var j = 0, jl = tags.length; j < jl; j++) {
8229 var tag = tags[j];
8230 var name = tag.textContent.bbbSpaceClean().replace(" ", "_");
8231 var collapse = data[name];
8232 var sibling = tag.nextElementSibling;
8233 nameList += name + " ";
8234
8235 tag.addEventListener("click", collapseSidebarToggle, false);
8236 tag.addEventListener("mouseup", collapseSidebarDefaultToggle.bind(null, name), false);
8237 tag.addEventListener("contextmenu", disableEvent, false);
8238
8239 if (collapse && sibling)
8240 sibling.bbbAddClass("bbb-collapsed-sidebar");
8241 }
8242 }
8243
8244 // Clean up potential old section names.
8245 for (i in data) {
8246 if (data.hasOwnProperty(i)) {
8247 if (nameList.indexOf(i.bbbSpacePad()) < 0) {
8248 removedOld = true;
8249 delete data[i];
8250 }
8251 }
8252 }
8253
8254 if (removedOld) {
8255 loadSettings();
8256 bbb.user.collapse_sidebar_data[dataLoc] = data;
8257 saveSettings();
8258 }
8259 }
8260
8261 function collapseSidebarToggle(event) {
8262 // Collapse/expand a sidebar section.
8263 var target = event.target;
8264 var sibling = target.nextElementSibling;
8265
8266 if (event.button !== 0 || !sibling)
8267 return;
8268
8269 if (sibling.bbbHasClass("bbb-collapsed-sidebar"))
8270 sibling.bbbRemoveClass("bbb-collapsed-sidebar");
8271 else
8272 sibling.bbbAddClass("bbb-collapsed-sidebar");
8273
8274 event.preventDefault();
8275 }
8276
8277 function collapseSidebarDefaultToggle(name, event) {
8278 // Make a sidebar section expand/collapse by default.
8279 if (event.button !== 2)
8280 return;
8281
8282 var dataLoc = (gLoc === "post" ? "post" : "thumb");
8283 var data = collapse_sidebar_data[dataLoc];
8284 var collapse = data[name];
8285
8286 loadSettings();
8287
8288 if (collapse) {
8289 delete bbb.user.collapse_sidebar_data[dataLoc][name];
8290 delete data[name];
8291 }
8292 else
8293 bbb.user.collapse_sidebar_data[dataLoc][name] = data[name] = true;
8294
8295 saveSettings();
8296
8297 bbbNotice("The \"" + name + "\" section will now " + (!collapse ? "collapse" : "expand") + " by default.", 3);
8298
8299 event.preventDefault();
8300 }
8301
8302 function allowUserLimit() {
8303 // 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.
8304 var page = Number(getVar("page")) || 1; // When set to 0 or undefined, the first page is shown.
8305 var queryLimit = getQueryLimit();
8306 var searchLimit = getSearchLimit();
8307 var limit = (queryLimit !== undefined ? queryLimit : searchLimit) || thumbnail_count_default;
8308 var allowedLoc = (gLoc === "search" || gLoc === "notes" || gLoc === "favorites");
8309
8310 if (allowedLoc && thumbnail_count && thumbnail_count !== limit && page === 1 && (searchLimit === undefined || queryLimit !== undefined))
8311 return true;
8312 else
8313 return false;
8314 }
8315
8316 function noResultsPage(pageEl) {
8317 // Check whether a page has zero results on it.
8318 var target = pageEl || document.body;
8319 var numPosts = getPosts(target).length;
8320 var thumbContainer = getThumbContainer(gLoc, target) || target;
8321 var thumbContainerText = (thumbContainer ? thumbContainer.textContent : "");
8322
8323 if (!numPosts && thumbContainerText.indexOf("Nobody here but us chickens") > -1)
8324 return true;
8325 else
8326 return false;
8327 }
8328
8329 function danbLoc(url) {
8330 // Test a URL to find which section of Danbooru the script is running on.
8331 var target;
8332
8333 if (url) {
8334 target = document.createElement("a");
8335 target.href = url;
8336 }
8337 else
8338 target = location;
8339
8340 var path = target.pathname;
8341 var query = target.search;
8342
8343 if (/\/posts\/\d+/.test(path))
8344 return "post";
8345 else if (/^\/(?:posts|$)/.test(path))
8346 return "search";
8347 else if (/^\/notes\/?$/.test(path) && query.indexOf("group_by=note") < 0)
8348 return "notes";
8349 else if (/\/comments\/\d+/.test(path))
8350 return "comment";
8351 else if (/^\/comments\/?$/.test(path)) {
8352 if (query.indexOf("group_by=comment") < 0)
8353 return "comments";
8354 else // This may need to be more specific in the future.
8355 return "comment_search";
8356 }
8357 else if (/\/explore\/posts\/popular(?:\/?$|\?)/.test(path))
8358 return "popular";
8359 // else if (/\/explore\/posts\/popular_view(?:\/?$|\?)/.test(path))
8360 // return "popular_view";
8361 else if (/\/pools\/\d+(?:\/?$|\?)/.test(path))
8362 return "pool";
8363 else if (/\/favorite_groups\/\d+(?:\/?$|\?)/.test(path))
8364 return "favorite_group";
8365 else if (/\/pools\/gallery/.test(path))
8366 return "pool_gallery";
8367 else if (path.indexOf("/favorites") === 0)
8368 return "favorites";
8369 else if (path.indexOf("/uploads/new") === 0)
8370 return "upload";
8371 else if (path.indexOf("/pools/new") === 0)
8372 return "new_pool";
8373 else if (/\/forum_topics\/\d+/.test(path))
8374 return "topic";
8375 else if (path.indexOf("/explore/posts/intro") === 0)
8376 return "intro";
8377 else
8378 return undefined;
8379 }
8380
8381 function isLoggedIn() {
8382 // Use Danbooru's meta tags to determine if a use is logged in.
8383 if (getMeta("current-user-id") !== "")
8384 return true;
8385 else
8386 return false;
8387 }
8388
8389 function noXML() {
8390 // Don't use XML requests on certain pages where it won't do any good.
8391 var limit = getLimit();
8392 var pageNum = getVar("page");
8393 var paginator = getPaginator();
8394 var thumbContainer = getThumbContainer();
8395 var imgContainer = getPostContent().container;
8396
8397 if (!paginator && !thumbContainer && !imgContainer)
8398 return true;
8399 else if (gLoc === "search" || gLoc === "favorites") {
8400 if (limit === 0 || pageNum === "b1" || noResultsPage() || safebSearchTest())
8401 return true;
8402 }
8403 else if (gLoc === "notes") {
8404 if (limit === 0 || noResultsPage())
8405 return true;
8406 }
8407 else if (gLoc === "comments") {
8408 if (pageNum === "b1" || noResultsPage())
8409 return true;
8410 }
8411 else if (gLoc === "pool" || gLoc === "favorite_group" || gLoc === "popular" || gLoc === "popular_view") {
8412 if (noResultsPage())
8413 return true;
8414 }
8415
8416 return false;
8417 }
8418
8419 function useAPI() {
8420 // Determine whether any options that require the API are enabled.
8421 if ((show_loli || show_shota || show_toddlercon || show_deleted || show_banned) && (isLoggedIn() || !bypass_api))
8422 return true;
8423 else
8424 return false;
8425 }
8426
8427 function removeInheritedStorage(key) {
8428 // Remove an inherited sessionStorage key for a new tab/window.
8429 if (window.opener && history.length === 1) {
8430 var state = history.state || {};
8431 var stateProperty = key + "_reset";
8432
8433 if (!state[stateProperty]) {
8434 sessionStorage.removeItem(key);
8435 state[stateProperty] = true;
8436 history.replaceState(state, "", location.href);
8437 }
8438 }
8439 }
8440
8441 function accountSettingCheck(scriptSetting) {
8442 // Determine whether the script setting or account/anonymous setting should be used.
8443 var loggedIn = isLoggedIn();
8444 var setting;
8445
8446 if (scriptSetting === "script_blacklisted_tags") {
8447 if ((loggedIn && override_blacklist === "always") || (!loggedIn && override_blacklist !== "disabled"))
8448 setting = bbb.user.script_blacklisted_tags;
8449 else
8450 setting = getMeta("blacklisted-tags") || "";
8451 }
8452 else if (scriptSetting === "post_resize") {
8453 if (loggedIn && !override_resize)
8454 setting = (getMeta("always-resize-images") === "true");
8455 else
8456 setting = bbb.user.post_resize;
8457 }
8458 else if (scriptSetting === "load_sample_first") {
8459 if (loggedIn && !override_sample)
8460 setting = (getMeta("default-image-size") === "large");
8461 else
8462 setting = bbb.user.load_sample_first;
8463 }
8464
8465 return setting;
8466 }
8467
8468 function safebPostTest(post) {
8469 // Test Safebooru's posts to see if they're censored or not.
8470 if (location.host.indexOf("safebooru") < 0)
8471 return false;
8472
8473 var postObject = post;
8474
8475 // If dealing with an element, turn it into a simple post info object.
8476 if (postObject instanceof HTMLElement) {
8477 if (postObject.tagName === "ARTICLE" || postObject.id === "image-container") {
8478 var tags = postObject.getAttribute("data-tags");
8479 var rating = postObject.getAttribute("data-rating");
8480
8481 postObject = {rating: rating, tag_string: tags};
8482 }
8483 }
8484
8485 // Posts with an explicit/questionable rating or censored tags are bad.
8486 if (post.rating !== "s" || safebCensorTagTest(post.tag_string))
8487 return true;
8488 else
8489 return false;
8490 }
8491
8492 function safebSearchTest() {
8493 // Test Safebooru's current search tags to see if there will be no results.
8494 if (location.host.indexOf("safebooru") < 0)
8495 return false;
8496
8497 var tags = getVar("tags");
8498 var tag;
8499 var i, il; // Loop variables.
8500
8501 if (!tags)
8502 return false;
8503
8504 tags = decodeURIComponent(tags.replace(/(\+|%20)/g, " ")).split(" ");
8505
8506 // Split up the "any" (~) tags and "all" tags for testing.
8507 var allTags = "";
8508 var anyTags = [];
8509
8510 for (i = 0, il = tags.length; i < il; i++) {
8511 tag = tags[i];
8512
8513 if (tag.charAt(0) === "~")
8514 anyTags.push(tag.slice(1));
8515 else
8516 allTags += " " + tag;
8517 }
8518
8519 // If any one "all" tag is censored, all posts will be bad.
8520 if (safebCensorTagTest(allTags))
8521 return true;
8522
8523 // If any one "any" tag isn't censored, not all posts will be bad.
8524 var anyMatch = !!anyTags[0];
8525
8526 for (i = 0, il = anyTags.length; i < il; i++) {
8527 tag = anyTags[i];
8528
8529 if (!safebCensorTagTest(tag)) {
8530 anyMatch = false;
8531 break;
8532 }
8533 }
8534
8535 return anyMatch;
8536 }
8537
8538 function safebCensorTagTest(string) {
8539 // Test a tag string or search string on Safebooru to see if it contains any bad tags.
8540 if (typeof(string) !== "string")
8541 return false;
8542 else
8543 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);
8544 }
8545
8546 function searchAdd() {
8547 // Choose the appropriate search link option to run.
8548 if (search_add === "disabled" || (gLoc !== "search" && gLoc !== "post" && gLoc !== "favorites"))
8549 return;
8550
8551 searchAddRemove();
8552
8553 if (search_add === "link")
8554 searchAddLink();
8555 else if (search_add === "toggle")
8556 searchAddToggleLink();
8557 }
8558
8559 function searchAddRemove() {
8560 // Completely remove existing + and - tag links along with the whitespace after them.
8561 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
8562
8563 if (!tagList)
8564 return;
8565
8566 var addLinks = tagList.getElementsByClassName("search-inc-tag");
8567 var subLinks = tagList.getElementsByClassName("search-exl-tag");
8568 var blankRegEx = /^\s*$/;
8569
8570 while (addLinks[0]) {
8571 var addLink = addLinks[0];
8572 var addSibling = addLink.nextSibling;
8573 var addParent = addSibling.parentNode;
8574
8575 if (addSibling && addSibling.nodeType === 3 && blankRegEx.test(addSibling.nodeValue))
8576 addParent.removeChild(addSibling);
8577
8578 addParent.removeChild(addLink);
8579 }
8580
8581 while (subLinks[0]) {
8582 var subLink = subLinks[0];
8583 var subSibling = subLink.nextSibling;
8584 var subParent = subSibling.parentNode;
8585
8586 if (subSibling && subSibling.nodeType === 3 && blankRegEx.test(subSibling.nodeValue))
8587 subParent.removeChild(subSibling);
8588
8589 subParent.removeChild(subLink);
8590 }
8591 }
8592
8593 function searchAddLink() {
8594 // Add + and - links to the sidebar tag list for modifying searches.
8595 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
8596
8597 if (!tagList)
8598 return;
8599
8600 var tagItems = tagList.getElementsByTagName("li");
8601 var curTag = getCurTags();
8602 var curTagString = (curTag ? "+" + curTag : "");
8603
8604 for (var i = 0, il = tagItems.length; i < il; i++) {
8605 var tagItem = tagItems[i];
8606 var tagLink = tagItem.getElementsByClassName("search-tag")[0];
8607 var tagString = getVar("tags", tagLink.href);
8608 var tagFrag = document.createDocumentFragment();
8609
8610 var addTag = document.createElement("a");
8611 addTag.href = "/posts?tags=" + tagString + curTagString;
8612 addTag.innerHTML = "+";
8613 addTag.className = "search-inc-tag";
8614 tagFrag.appendChild(addTag);
8615
8616 var addSpace = document.createTextNode(" ");
8617 tagFrag.appendChild(addSpace);
8618
8619 var subTag = document.createElement("a");
8620 subTag.href = "/posts?tags=-" + tagString + curTagString;
8621 subTag.innerHTML = "–";
8622 subTag.className = "search-exl-tag";
8623 tagFrag.appendChild(subTag);
8624
8625 var subSpace = document.createTextNode(" ");
8626 tagFrag.appendChild(subSpace);
8627
8628 tagItem.insertBefore(tagFrag, tagLink);
8629 }
8630 }
8631
8632 function searchAddToggleLink() {
8633 // Add toggle links to the sidebar tag list for modifying the search box value.
8634 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
8635
8636 if (!tagList)
8637 return;
8638
8639 var tagItems = tagList.getElementsByTagName("li");
8640 var firstItem = tagItems[0];
8641 var toggleWidth;
8642
8643 // Find a set width for the toggle link.
8644 if (firstItem) {
8645 var testItem = document.createElement("li");
8646 testItem.className = "category-0";
8647 testItem.style.height = "0px";
8648 testItem.style.visibility = "hidden";
8649
8650 var testLink = document.createElement("a");
8651 testLink.href = "#";
8652 testLink.style.display = "inline-block";
8653 testItem.appendChild(testLink);
8654
8655 firstItem.parentNode.appendChild(testItem);
8656
8657 testLink.innerHTML = "-";
8658 var subWidth = testLink.clientWidth;
8659
8660 testLink.innerHTML = "+";
8661 var addWidth = testLink.clientWidth;
8662
8663 testLink.innerHTML = "~";
8664 var orWidth = testLink.clientWidth;
8665
8666 toggleWidth = Math.max(subWidth, addWidth, orWidth);
8667
8668 firstItem.parentNode.removeChild(testItem);
8669 }
8670
8671 // Create and insert the toggle links.
8672 for (var i = 0, il = tagItems.length; i < il; i++) {
8673 var tagItem = tagItems[i];
8674 var tagLink = tagItem.getElementsByClassName("search-tag")[0];
8675 var tagString = decodeURIComponent(getVar("tags", tagLink.href));
8676 var tagFrag = document.createDocumentFragment();
8677 var tagFunc = searchAddToggle.bind(null, tagString);
8678
8679 var toggleTag = document.createElement("a");
8680 toggleTag.href = "/posts?tags=" + tagString;
8681 toggleTag.innerHTML = "»";
8682 toggleTag.style.display = "inline-block";
8683 toggleTag.style.textAlign = "center";
8684 toggleTag.style.width = toggleWidth + "px";
8685 toggleTag.addEventListener("click", tagFunc, false);
8686 toggleTag.addEventListener("mouseup", tagFunc, false);
8687 toggleTag.addEventListener("contextmenu", disableEvent, false);
8688 tagFrag.appendChild(toggleTag);
8689
8690 var toggleSpace = document.createTextNode(" ");
8691 tagFrag.appendChild(toggleSpace);
8692
8693 tagItem.insertBefore(tagFrag, tagLink);
8694 bbb.search_add.links[tagString] = toggleTag;
8695 }
8696
8697 // Watch various actions on the search box.
8698 var tagsInput = document.getElementById("tags");
8699
8700 if (tagsInput && (gLoc === "search" || gLoc === "post" || gLoc === "intro" || gLoc === "favorites")) {
8701 searchAddToggleCheck();
8702 tagsInput.addEventListener("input", searchAddToggleCheck, false);
8703 tagsInput.addEventListener("keyup", searchAddToggleCheck, false);
8704 tagsInput.addEventListener("cut", searchAddToggleCheck, false);
8705 tagsInput.addEventListener("paste", searchAddToggleCheck, false);
8706 tagsInput.addEventListener("change", searchAddToggleCheck, false);
8707 $(tagsInput).on("autocompleteselect", function(event) { delayMe(function(event) { searchAddToggleCheck(event); }); }); // Delayed to allow autocomplete to change the input.
8708 }
8709 }
8710
8711 function searchAddToggleCheck(event) {
8712 // Watch the search box value, test it upon changes, and update the tag links accordingly.
8713 var input = (event ? event.target : document.getElementById("tags"));
8714 var value = input.value;
8715 var oldValue = bbb.search_add.old;
8716 var i, il; // Loop variables.
8717
8718 if (oldValue !== value) {
8719 var tags = value.toLowerCase().bbbSpaceClean().split(/\s+/);
8720 var activeLinks = bbb.search_add.active_links;
8721
8722 for (i in activeLinks) {
8723 if (activeLinks.hasOwnProperty(i)) {
8724 var activeRegEx = new RegExp("(?:^|\\s)[-~]*" + escapeRegEx(i) + "(?:$|\\s)", "gi");
8725
8726 if (!activeRegEx.test(value))
8727 activeLinks[i].innerHTML = "»";
8728 }
8729 }
8730
8731 for (i = 0, il = tags.length; i < il; i++) {
8732 var tag = tags[i];
8733 var tagChar = tag.charAt(0);
8734 var tagType = "+";
8735
8736 if (tagChar === "-" || tagChar === "~") {
8737 tagType = (tagChar === "-" ? "–" : tagChar);
8738 tag = tag.slice(1);
8739 }
8740
8741 var tagLink = bbb.search_add.links[tag];
8742
8743 if (tagLink) {
8744 bbb.search_add.links[tag].innerHTML = tagType;
8745 bbb.search_add.active_links[tag] = tagLink;
8746 }
8747 }
8748
8749 bbb.search_add.old = value;
8750 }
8751 }
8752
8753 function searchAddToggle(tag, event) {
8754 // Modify the search box value based upon the tag link clicked and the tag's current state.
8755 var link = event.target;
8756 var button = event.button;
8757 var type = event.type;
8758 var linkType = link.innerHTML;
8759 var input = document.getElementById("tags");
8760 var inputValue = input.value;
8761 var tagRegEx = new RegExp("(^|\\s)[-~]*" + escapeRegEx(tag) + "(?=$|\\s)", "gi");
8762 var angleQuotes = String.fromCharCode(187);
8763 var enDash = String.fromCharCode(8211);
8764
8765 if (button === 2) // Immediately remove the tag upon a right click.
8766 linkType = "~";
8767 else if (button !== 0 || (type === "mouseup" && button !== 2) || (type === "click" && button !== 0))
8768 return;
8769
8770 // Each case changes the tag's toggle link display and updates the search box.
8771 switch (linkType) {
8772 case angleQuotes: // Tag currently not present.
8773 link.innerHTML = "+";
8774
8775 if (tagRegEx.test(inputValue))
8776 input.value = inputValue.replace(tagRegEx, tag).bbbSpaceClean();
8777 else
8778 input.value = (inputValue + " " + tag).bbbSpaceClean();
8779 break;
8780 case "+": // Tag currently included.
8781 link.innerHTML = "–";
8782
8783 if (tagRegEx.test(inputValue))
8784 input.value = inputValue.replace(tagRegEx, "$1-" + tag).bbbSpaceClean();
8785 else
8786 input.value = (inputValue + " -" + tag).bbbSpaceClean();
8787 break;
8788 case enDash: // Tag currently excluded.
8789 link.innerHTML = "~";
8790
8791 if (tagRegEx.test(inputValue))
8792 input.value = inputValue.replace(tagRegEx, "$1~" + tag).bbbSpaceClean();
8793 else
8794 input.value = (inputValue + " ~" + tag).bbbSpaceClean();
8795 break;
8796 case "~": // Tag currently included with other tags.
8797 link.innerHTML = angleQuotes;
8798
8799 if (tagRegEx.test(inputValue))
8800 input.value = inputValue.replace(tagRegEx, "$1").bbbSpaceClean();
8801 break;
8802 }
8803
8804 event.preventDefault();
8805 }
8806
8807 function localStorageDialog() {
8808 // Open a dialog box for cleaning out local storage for donmai.us.
8809 if (getCookie().bbb_ignore_storage)
8810 return;
8811
8812 var domains = [
8813 {url: "http://danbooru.donmai.us/", untrusted: false},
8814 {url: "https://danbooru.donmai.us/", untrusted: false},
8815 {url: "http://donmai.us/", untrusted: false},
8816 {url: "https://donmai.us/", untrusted: true},
8817 {url: "http://sonohara.donmai.us/", untrusted: false},
8818 {url: "https://sonohara.donmai.us/", untrusted: true},
8819 {url: "http://hijiribe.donmai.us/", untrusted: false},
8820 {url: "https://hijiribe.donmai.us/", untrusted: true},
8821 {url: "http://safebooru.donmai.us/", untrusted: false},
8822 {url: "https://safebooru.donmai.us/", untrusted: false},
8823 {url: "http://testbooru.donmai.us/", untrusted: false}
8824 ];
8825
8826 var content = document.createDocumentFragment();
8827
8828 var header = document.createElement("h2");
8829 header.innerHTML = "Local Storage Error";
8830 header.className = "bbb-header";
8831 content.appendChild(header);
8832
8833 var introText = document.createElement("div");
8834 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 and BBB's thumbnail info 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 and thumbnail info.</li></ul><br> <b>Donmai.us domains/subdomains:</b><br>";
8835 content.appendChild(introText);
8836
8837 var domainDiv = document.createElement("div");
8838 domainDiv.style.lineHeight = "1.5em";
8839 content.appendChild(domainDiv);
8840
8841 var cbFunc = function(event) {
8842 var target = event.target;
8843
8844 target.nextSibling.style.textDecoration = (target.checked ? "none" : "line-through");
8845 };
8846
8847 for (var i = 0, il = domains.length; i < il; i++) {
8848 var domain = domains[i];
8849 var isChecked = (!domain.untrusted || location.href.indexOf(domain.url) > -1);
8850
8851 var listCheckbox = document.createElement("input");
8852 listCheckbox.name = domain.url;
8853 listCheckbox.type = "checkbox";
8854 listCheckbox.checked = isChecked;
8855 listCheckbox.style.marginRight = "5px";
8856 listCheckbox.addEventListener("click", cbFunc, false);
8857 domainDiv.appendChild(listCheckbox);
8858
8859 var listLink = document.createElement("a");
8860 listLink.innerHTML = domain.url + (domain.untrusted ? " (untrusted)" : "");
8861 listLink.href = domain.url;
8862 listLink.target = "_blank";
8863 listLink.style.textDecoration = (isChecked ? "none" : "line-through");
8864 domainDiv.appendChild(listLink);
8865
8866 var br = document.createElement("br");
8867 domainDiv.appendChild(br);
8868 }
8869
8870 var optionsText = document.createElement("div");
8871 optionsText.innerHTML = "<b>Options:</b><br>";
8872 optionsText.style.marginTop = "1em";
8873 content.appendChild(optionsText);
8874
8875 var optionsDiv = document.createElement("div");
8876 optionsDiv.style.lineHeight = "1.5em";
8877 content.appendChild(optionsDiv);
8878
8879 var compCheckbox = document.createElement("input");
8880 compCheckbox.name = "complete-delete";
8881 compCheckbox.type = "checkbox";
8882 compCheckbox.style.marginRight = "5px";
8883 optionsDiv.appendChild(compCheckbox);
8884
8885 var compText = document.createTextNode("Delete everything in local storage for each selection except for my BBB settings.");
8886 optionsDiv.appendChild(compText);
8887
8888 var okFunc = function() {
8889 var options = domainDiv.getElementsByTagName("input");
8890 var mode = (compCheckbox.checked ? "complete" : "normal");
8891 var selectedURLs = [];
8892 var origURL = location.href;
8893 var nextURL;
8894 var cleanCur = false;
8895 var session = new Date().getTime();
8896
8897 for (var i = 0, il = options.length; i < il; i++) {
8898 var option = options[i];
8899
8900 if (option.checked) {
8901 if (origURL.indexOf(option.name) === 0)
8902 cleanCur = true;
8903 else if (!nextURL)
8904 nextURL = option.name;
8905 else
8906 selectedURLs.push(encodeURIComponent(option.name));
8907 }
8908 }
8909
8910 // Clean the current domain if it was selected.
8911 if (cleanCur)
8912 cleanLocalStorage(mode);
8913
8914 if (!nextURL) {
8915 // Retry saving if only the current domain was selected and do nothing if no domains were selected.
8916 if (cleanCur)
8917 retryLocalStorage();
8918 }
8919 else {
8920 // Start cycling through domains.
8921 bbbDialog("Currently cleaning local storage and loading the next domain. Please wait...", {ok: false, important: true});
8922 sessionStorage.bbbSetItem("bbb_local_storage_queue", JSON.stringify(bbb.local_storage_queue));
8923 location.href = updateURLQuery(nextURL + "posts/1/", {clean_storage: mode, clean_urls: selectedURLs.join(","), clean_origurl: encodeURIComponent(origURL), clean_session: session});
8924 }
8925 };
8926
8927 var cancelFunc = function() {
8928 createCookie("bbb_ignore_storage", 1);
8929 };
8930
8931 bbbDialog(content, {ok: okFunc, cancel: cancelFunc});
8932 }
8933
8934 function cleanLocalStorage(mode) {
8935 // Clean out various values in local storage.
8936 var keyName;
8937 var i;
8938
8939 if (mode === "autocomplete") {
8940 for (i = localStorage.length - 1; i >= 0; i--) {
8941 keyName = localStorage.key(i);
8942
8943 if (keyName.indexOf("ac-") === 0)
8944 localStorage.removeItem(keyName);
8945 }
8946 }
8947 else if (mode === "normal") {
8948 for (i = localStorage.length - 1; i >= 0; i--) {
8949 keyName = localStorage.key(i);
8950
8951 if (keyName.indexOf("ac-") === 0 || keyName === "bbb_thumb_cache")
8952 localStorage.removeItem(keyName);
8953 }
8954 }
8955 else if (mode === "complete") {
8956 for (i = localStorage.length - 1; i >= 0; i--) {
8957 keyName = localStorage.key(i);
8958
8959 if (keyName !== "bbb_settings")
8960 localStorage.removeItem(keyName);
8961 }
8962 }
8963 }
8964
8965 function retryLocalStorage() {
8966 // Try to save items to local storage that failed to get saved before.
8967 var sessLocal;
8968
8969 if (sessionStorage.getItem("bbb_local_storage_queue")) {
8970 // Retrieve the local storage values from session storage after cycling through other domains.
8971 sessLocal = JSON.parse(sessionStorage.getItem("bbb_local_storage_queue"));
8972 sessionStorage.removeItem("bbb_local_storage_queue");
8973 }
8974 else if (bbb.local_storage_queue) {
8975 // If only the BBB storage object exists, assume the user selected to only clean the current domain and reset things.
8976 sessLocal = bbb.local_storage_queue;
8977 delete bbb.local_storage_queue;
8978 delete bbb.flags.local_storage_full;
8979 }
8980 else
8981 return;
8982
8983 for (var i in sessLocal) {
8984 if (sessLocal.hasOwnProperty(i))
8985 localStorage.bbbSetItem(i, sessLocal[i]);
8986 }
8987
8988 bbbNotice("Local storage cleaning has completed.", 6);
8989 }
8990
8991 function localStorageCheck() {
8992 // Check if the script is currently trying to manage local storage.
8993 var cleanMode = getVar("clean_storage");
8994 var cleanSession = Number(getVar("clean_session")) || 0;
8995 var session = new Date().getTime();
8996
8997 // Stop if the script is not currently cleaning storage or if an old URL is detected.
8998 if (!cleanMode || Math.abs(session - cleanSession) > 60000)
8999 return;
9000
9001 if (cleanMode !== "save") {
9002 // Cycle through the domains.
9003 var urls = getVar("clean_urls").split(",");
9004 var nextURL = urls.shift();
9005 var origURL = getVar("clean_origurl");
9006
9007 bbb.flags.local_storage_full = true; // Keep the cycled domains from triggering storage problems
9008 bbbDialog("Currently cleaning local storage and loading the next domain. Please wait...", {ok: false, important: true});
9009 history.replaceState((history.state || {}), "", updateURLQuery(location.href, {clean_storage: undefined, clean_urls: undefined, clean_origurl: undefined, clean_session: undefined}));
9010 cleanLocalStorage(cleanMode);
9011
9012 if (nextURL)
9013 window.setTimeout(function() { location.href = updateURLQuery(decodeURIComponent(nextURL) + "posts/1/", {clean_storage: cleanMode, clean_urls: urls.join(), clean_origurl: origURL, clean_session: session}); }, 2000);
9014 else
9015 window.setTimeout(function() { location.href = updateURLQuery(decodeURIComponent(origURL), {clean_storage: "save", clean_session: session}); }, 2000);
9016 }
9017 else if (cleanMode === "save") {
9018 history.replaceState((history.state || {}), "", updateURLQuery(location.href, {clean_storage: undefined, clean_session: undefined}));
9019 retryLocalStorage();
9020 }
9021 }
9022
9023 function getCookie() {
9024 // Return an associative array with cookie values.
9025 var data = document.cookie;
9026
9027 if(!data)
9028 return false;
9029
9030 data = data.split("; ");
9031 var out = [];
9032
9033 for (var i = 0, il = data.length; i < il; i++) {
9034 var temp = data[i].split("=");
9035 out[temp[0]] = temp[1];
9036 }
9037
9038 return out;
9039 }
9040
9041 function createCookie(cName, cValue, expDays) {
9042 // Generate a cookie with a expiration time in days.
9043 var data = cName + "=" + cValue + "; path=/";
9044
9045 if (expDays !== undefined) {
9046 var expDate = new Date();
9047 expDate.setTime(expDate.getTime() + expDays * 86400000);
9048 expDate.toUTCString();
9049 data += "; expires=" + expDate;
9050 }
9051
9052 document.cookie = data;
9053 }
9054
9055 function scrollbarWidth() {
9056 // Retrieve the scrollbar width by creating an element with scrollbars and finding the difference.
9057 var scroller = document.createElement("div");
9058 scroller.style.width = "150px";
9059 scroller.style.height = "150px";
9060 scroller.style.visibility = "hidden";
9061 scroller.style.overflow = "scroll";
9062 scroller.style.position = "absolute";
9063 scroller.style.top = "0px";
9064 scroller.style.left = "0px";
9065 document.body.appendChild(scroller);
9066 var scrollDiff = scroller.offsetWidth - scroller.clientWidth;
9067 document.body.removeChild(scroller);
9068
9069 return scrollDiff;
9070 }
9071
9072 function bbbIsNum(value) {
9073 // Strictly test for a specific style of number.
9074 return /^-?\d+(\.\d+)?$/.test(value);
9075 }
9076
9077 function isNumMetatag(tag) {
9078 // Check if the tag from a search string is a numeric metatag.
9079 if (tag.indexOf(":") < 0)
9080 return false;
9081 else {
9082 var tagName = tag.split(":", 1)[0];
9083
9084 if (tagName === "score" || tagName === "favcount" || tagName === "id" || tagName === "width" || tagName === "height")
9085 return true;
9086 else
9087 return false;
9088 }
9089 }
9090
9091 function isMetatag(tag) {
9092 // Check if the tag from a search string is a metatag.
9093 if (tag.indexOf(":") < 0)
9094 return false;
9095 else {
9096 var tagName = tag.split(":", 1)[0].bbbSpaceClean();
9097
9098 if (tagName === "pool" || tagName === "user" || tagName === "status" || tagName === "rating" || tagName === "parent" || tagName === "child")
9099 return true;
9100 else
9101 return false;
9102 }
9103 }
9104
9105 function delayMe(func) {
9106 // Run the function after the browser has finished its current stack of tasks.
9107 window.setTimeout(func, 0);
9108 }
9109
9110 function escapeRegEx(regEx) {
9111 // Replace special characters with escaped versions to make them safe for RegEx.
9112 return regEx.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
9113 }
9114
9115 function updateURLQuery(url, newQueries) {
9116 // Update the query portion of a URL. If a param isn't declared, it will be added. If it is, it will be updated.
9117 // 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.
9118 var urlParts = url.split(/[?#]/g, 2);
9119 var urlQuery = urlParts[1] || "";
9120 var queries = urlQuery.split("&");
9121 var query;
9122 var queryName;
9123 var queryValue;
9124 var queryObj = {};
9125 var newUrl;
9126 var i, il; // Loop variables.
9127
9128 for (i = 0, il = queries.length; i < il; i++) {
9129 query = queries[i].split("=");
9130 queryName = query[0];
9131 queryValue = query[1];
9132
9133 if (queryName)
9134 queryObj[queryName] = queryValue;
9135 }
9136
9137 for (i in newQueries) {
9138 if (newQueries.hasOwnProperty(i))
9139 queryObj[i] = newQueries[i];
9140 }
9141
9142 queries.length = 0;
9143
9144 for (i in queryObj) {
9145 if (queryObj.hasOwnProperty(i)) {
9146 queryValue = queryObj[i];
9147
9148 if (queryValue === null) // Declared param with no assigned value.
9149 query = i;
9150 else if (queryValue === undefined) // Undeclared.
9151 query = undefined;
9152 else // Declared param with an assigned value (including empty srings).
9153 query = i + "=" + queryValue;
9154
9155 if (query !== undefined) // Omit undefined params.
9156 queries.push(query);
9157 }
9158 }
9159
9160 urlQuery = queries.join("&");
9161 newUrl = urlParts[0] + (urlQuery ? "?" + urlQuery : "");
9162
9163 return newUrl;
9164 }
9165
9166 function timestamp(format) {
9167 // Returns a simple timestamp based on the format string provided. String placeholders: y = year, m = month, d = day, hh = hours, mm = minutes, ss = seconds
9168 function padDate(number) {
9169 // Adds a leading "0" to single digit values.
9170 var numString = String(number);
9171
9172 if (numString.length === 1)
9173 numString = "0" + numString;
9174
9175 return numString;
9176 }
9177
9178 var stamp = format || "y-m-d hh:mm:ss";
9179 var time = new Date();
9180 var year = time.getFullYear();
9181 var month = padDate(time.getMonth() + 1);
9182 var day = padDate(time.getDate());
9183 var hours = padDate(time.getHours());
9184 var minutes = padDate(time.getMinutes());
9185 var seconds = padDate(time.getSeconds());
9186
9187 stamp = stamp.replace("hh", hours).replace("mm", minutes).replace("ss", seconds).replace("y", year).replace("m", month).replace("d", day);
9188
9189 return stamp;
9190 }
9191
9192 function isOldVersion(ver) {
9193 // Takes the provided version and compares it to the script version. Returns true if the provided version is older than the script version.
9194 var userVer = ver || bbb.user.bbb_version;
9195 var scriptVer = bbb.options.bbb_version;
9196 var userNums = userVer.split(".");
9197 var userLength = userNums.length;
9198 var scriptNums = scriptVer.split(".");
9199 var scriptLength = scriptNums.length;
9200 var loopLength = (userLength > scriptLength ? userLength : scriptLength);
9201
9202 for (var i = 0; i < loopLength; i++) {
9203 var userNum = (userNums[i] ? Number(userNums[i]) : 0);
9204 var scriptNum = (scriptNums[i] ? Number(scriptNums[i]) : 0);
9205
9206 if (userNum < scriptNum)
9207 return true;
9208 else if (scriptNum < userNum)
9209 return false;
9210 }
9211
9212 return false;
9213 }
9214
9215 function uniqueIdNum() {
9216 // Return a unique ID number for an element.
9217 if (!bbb.uId)
9218 bbb.uId = 1;
9219 else
9220 bbb.uId++;
9221
9222 return "bbbuid" + bbb.uId;
9223 }
9224
9225} // End of bbbScript.
9226
9227function runBBBScript() {
9228 // Run the script or prep it to run when Danbooru's JS is ready.
9229 if (document.readyState === "interactive" && typeof(Danbooru) === "undefined") {
9230 var danbScripts = document.getElementsByTagName("script");
9231
9232 for (var i = 0, il = danbScripts.length; i < il; i++) {
9233 var curScript = danbScripts[i];
9234 var curScriptSrc = curScript.src || "";
9235
9236 if (curScriptSrc.indexOf("donmai.us/assets/application-") > -1) {
9237 curScript.addEventListener("load", bbbScript, true);
9238 break;
9239 }
9240 }
9241 }
9242 else
9243 bbbScript();
9244}
9245
9246function testBBBAccess() {
9247 // Check whether the script has access to the page.
9248 if (!document.body)
9249 return;
9250
9251 function testFunc() {
9252 window.bbb_access = true;
9253 }
9254
9255 var testScript = document.createElement('script');
9256 testScript.type = "text/javascript";
9257 testScript.appendChild(document.createTextNode('(' + testFunc + ')();'));
9258 document.body.appendChild(testScript);
9259 window.setTimeout(function() { document.body.removeChild(testScript); }, 0);
9260
9261 if (!window.bbb_access) { // Embed the script since it can't get to Danbooru's JS.
9262 var script = document.createElement('script');
9263 script.type = "text/javascript";
9264 script.appendChild(document.createTextNode(bbbScript));
9265 script.appendChild(document.createTextNode('(' + runBBBScript + ')();'));
9266 document.body.appendChild(script);
9267 }
9268 else // Operate normally.
9269 runBBBScript();
9270}
9271
9272testBBBAccess();