Quest Reader

Makes it more convenient to read quests

От 24.08.2019. Виж последната версия.

  1. // ==UserScript==
  2. // @name Quest Reader
  3. // @author naileD
  4. // @namespace QuestReader
  5. // @include *//tgchan.org/kusaba/quest/res/*
  6. // @include *//tgchan.org/kusaba/questarch/res/*
  7. // @include *//tgchan.org/kusaba/graveyard/res/*
  8. // @description Makes it more convenient to read quests
  9. // @version 9
  10. // @grant none
  11. // @icon data:image/vnd.microsoft.icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsSAAALEgHS3X78AAAANklEQVQokWNgoBOI2mJKpEomMvQgNAxRPUy4JGjjJJqoZoSrZmBgWOZzGlk/mlKILBMafxAAAE1pG/UEXzMMAAAAAElFTkSuQmCC
  12. // ==/UserScript==
  13. "use strict";
  14. //entry point is more or less at the end of the script
  15.  
  16. //enum
  17. const PostType = { UPDATE: 0, AUTHORCOMMENT: 1, SUGGESTION: 2, COMMENT: 3 };
  18.  
  19. //UpdateAnalyzer class
  20. //Input: document of the quest
  21. //Output: a Map object with all the quest's posts, where keys are post IDs and values are post types. The post types are Update (0), AuthorComment (1), Suggestion (2), Comment (3); There's no comments... yet.
  22. //Usage: var results = new UpdateAnalyzer().processQuest(document);
  23. class UpdateAnalyzer {
  24. constructor(options) {
  25. this.regex = UpdateAnalyzer.getRegexes();
  26. if (options) {
  27. this.postCache = null; //Used to transfer posts cache to/from this class. Used for debugging purposes.
  28. this.useCache = options.useCache; //Used for debugging purposes.
  29. this.debug = options.debug;
  30. this.debugAfterDate = options.debugAfterDate;
  31. }
  32. }
  33.  
  34. analyzeQuest(questDoc) {
  35. var posts = !this.postCache ? this.getPosts(questDoc) : JSON.parse(new TextDecoder().decode(this.postCache));
  36. var authorID = posts[0].userID; //authodID is the userID of the first post
  37. this.threadID = posts[0].postID; //threadID is the postID of the first post
  38.  
  39. this.totalFiles = this.getTotalFileCount(posts);
  40. var questFixes = this.getFixes(this.threadID); //for quests where we can't correctly determine authors and updates, we use a built-in database of fixes
  41. if (this.debug && (questFixes.imageQuest !== undefined || Object.values(questFixes).some(fix => Object.values(fix).length > 0))) { console.log(`Quest has manual fixes`); console.log(questFixes); }
  42. var graphData = this.getUserGraphData(posts, questFixes, authorID); //get user names as nodes and edges for building user graph
  43. var users = this.buildUserGraph(graphData.nodes, graphData.edges); //build a basic user graph... whatever that means!
  44. this.author = this.find(users[authorID]);
  45. this.getUserPostAndFileCounts(posts, users, questFixes); //count the amount of posts and files each user made
  46. this.imageQuest = this.isImageQuest(questFixes); //image quest is when the author posts files at least 50% of the time
  47. if (this.debug) console.log(`Image quest: ${this.imageQuest}`);
  48. if (this.imageQuest) { //in case this is an image quest, merge users a bit differently
  49. users = this.buildUserGraph(graphData.nodes, graphData.edges, graphData.strongNodes, authorID); //build the user graph again, but with some restrictions
  50. this.author = this.find(users[authorID]);
  51. this.processFilePosts(posts, users, questFixes); //analyze file names and merge users based on when one file name is predicted from another
  52. this.getUserPostAndFileCounts(posts, users, questFixes); //count the amount of posts and files each user posted
  53. this.mergeCommonFilePosters(posts, users, questFixes); //merge certain file-posting users with the quest author
  54. this.mergeMajorityFilePoster(posts, users, questFixes); //consider a user who posted 50%+ of the files in the thread as the author
  55. }
  56. var postUsers = this.setPostUsers(posts, users, questFixes); //do final user resolution
  57. var postTypes = this.getFinalPostTypes(posts, questFixes); //determine which posts are updates
  58. return { postTypes: postTypes, postUsers: postUsers };
  59. }
  60.  
  61. getPosts(questDoc) {
  62. var defaultName = "Suggestion";
  63. var posts = {}; //dictionary => postID / post object
  64. questDoc.querySelectorAll(".postwidth").forEach(postHeaderElement => { //querySelectorAll is FASTER than getElementsByClassName when DOM is large
  65. var postID = parseInt(postHeaderElement.querySelector("span[id^=dnb]").id.split("-")[2]);
  66. if (posts[postID]) { //checking this may seem unnecessary, but it's required for compatibility with some imageboard scripts
  67. return;
  68. }
  69. var uid, name, trip, subject, fileElement, fileExt, fileName = "", activeContent, contentElement;
  70. var uidElement = postHeaderElement.querySelector(".uid");
  71. uid = uidElement.textContent.substring(4);
  72. trip = postHeaderElement.querySelector(".postertrip");
  73. if (trip) { //use tripcode instead of name if it exists
  74. name = trip.textContent;
  75. }
  76. else {
  77. name = postHeaderElement.querySelector(".postername").textContent.trim();
  78. name = name == defaultName ? "" : name.toLowerCase();
  79. }
  80. subject = postHeaderElement.querySelector(".filetitle");
  81. subject = subject ? subject.textContent.trim() : "";
  82. fileElement = postHeaderElement.querySelector(".filesize");
  83. if (fileElement) { //try to get the original file name
  84. fileName = fileElement.getElementsByTagName("a")[0].href;
  85. var match = fileName.match(this.regex.fileExtension);
  86. fileExt = match ? match[0] : ""; //don't need .toLowerCase()
  87. if (fileExt == ".png" || fileExt == ".gif" || fileExt == ".jpg" || fileExt == ".jpeg") {
  88. var fileInfo = fileElement.lastChild.textContent.split(", ");
  89. if (fileInfo.length >= 3) {
  90. fileName = fileInfo[2].split("\n")[0];
  91. }
  92. }
  93. else {
  94. fileName = fileName.substr(fileName.lastIndexOf("/") + 1); //couldn't find original file name, use file name from the server instead
  95. }
  96. fileName = fileName.replace(this.regex.fileExtension, ""); //ignore file's extension
  97. }
  98. contentElement = postHeaderElement.nextElementSibling;
  99. activeContent = contentElement.querySelector("img, iframe") ? true : false; //does a post contain icons
  100. var postData = { postID: postID, userID: uid, userName: name, fileName: fileName, activeContent: activeContent };
  101. if (this.useCache) {
  102. postData.textUpdate = this.regex.fraction.test(subject) || this.containsQuotes(contentElement);
  103. }
  104. else {
  105. postData.subject = subject;
  106. postData.contentElement = contentElement;
  107. }
  108. if (this.useCache || this.debug || this.debugAfterDate) {
  109. postData.date = Date.parse(postHeaderElement.querySelector("label").lastChild.nodeValue);
  110. }
  111. posts[postID] = postData;
  112. });
  113. var postsArray = Object.values(posts); //convert to an array
  114. if (this.useCache) { //We stringify the object into JSON and then encode it into a Uint8Array to save space, otherwise the database would be too large
  115. this.postCache = new TextEncoder().encode(Object.toJSON ? Object.toJSON(postsArray) : JSON.stringify(postsArray)); //JSON.stringify stringifies twice when used on array. Another TGchan's protoaculous bug.
  116. }
  117. return postsArray;
  118. }
  119.  
  120. getTotalFileCount(posts) {
  121. var totalFileCount = 0;
  122. posts.forEach(post => { if (post.fileName || post.activeContent) totalFileCount++; });
  123. return totalFileCount;
  124. }
  125.  
  126. isImageQuest(questFixes, ignore) {
  127. if (questFixes.imageQuest !== undefined) {
  128. return questFixes.imageQuest;
  129. }
  130. else {
  131. return (this.author.fileCount / this.author.postCount) >= 0.5;
  132. }
  133. }
  134.  
  135. getUserGraphData(posts, questFixes, authorID) {
  136. var graphData = { nodes: new Set(), strongNodes: new Set(), edges: {} };
  137. posts.forEach(post => {
  138. graphData.nodes.add(post.userID);
  139. if (post.userName) {
  140. graphData.nodes.add(post.userName);
  141. graphData.edges[`${post.userID}${post.userName}`] = { E1: post.userID, E2: post.userName };
  142. }
  143. if (post.fileName || post.activeContent) { //strong nodes are user IDs that posted files
  144. graphData.strongNodes.add(post.userID);
  145. if (post.userName) {
  146. graphData.strongNodes.add(post.userName);
  147. }
  148. if (post.fileName && post.activeContent && post.userID != authorID) { //users that made posts with both file and icons are most likely the author
  149. graphData.edges[`${authorID}${post.userID}`] = { E1: authorID, E2: post.userID, hint: "fileAndIcons" };
  150. }
  151. }
  152. });
  153. for (var missedID in questFixes.missedAuthors) { //add missing links to the author from manual fixes
  154. graphData.edges[`${authorID}${missedID}`] = { E1: authorID, E2: missedID, hint: "missedAuthors" };
  155. graphData.strongNodes.add(missedID);
  156. }
  157. graphData.edges = Object.values(graphData.edges);
  158. return graphData;
  159. }
  160.  
  161. buildUserGraph(nodes, edges, strongNodes, authorID) {
  162. var users = {};
  163. var edgesSet = new Set(edges);
  164. nodes.forEach(node => {
  165. users[node] = this.makeSet(node);
  166. });
  167. if (!strongNodes) {
  168. edgesSet.forEach(edge => this.union(users[edge.E1], users[edge.E2]));
  169. }
  170. else {
  171. edgesSet.forEach(edge => { //merge strong with strong and weak with weak
  172. if ((strongNodes.has(edge.E1) && strongNodes.has(edge.E2)) || (!strongNodes.has(edge.E1) && !strongNodes.has(edge.E2))) {
  173. this.union(users[edge.E1], users[edge.E2]);
  174. edgesSet.delete(edge);
  175. }
  176. });
  177. var author = this.find(users[authorID]);
  178. edgesSet.forEach(edge => { //merge strong with weak, but only for users which aren't the author
  179. if (this.find(users[edge.E1]) != author && this.find(users[edge.E2]) != author) {
  180. this.union(users[edge.E1], users[edge.E2]);
  181. }
  182. });
  183. }
  184. return users;
  185. }
  186.  
  187. processFilePosts(posts, users, questFixes) {
  188. var last2Files = new Map();
  189. var filePosts = posts.filter(post => post.fileName && !questFixes.wrongImageUpdates[post.postID]);
  190. filePosts.forEach(post => {
  191. var postUser = this.find(users[post.userID]);
  192. var postFileName = post.fileName.match(this.regex.lastNumber) ? post.fileName : null; //no use processing files without numbers
  193. if (post.userName && this.find(users[post.userName]) == this.author) {
  194. postUser = this.author;
  195. }
  196. if (!last2Files.has(postUser)) {
  197. last2Files.set(postUser, [ null, null ]);
  198. }
  199. last2Files.get(postUser).shift();
  200. last2Files.get(postUser).push(postFileName);
  201. last2Files.forEach((last2, user) => {
  202. if (user == postUser) {
  203. return;
  204. }
  205. if ((last2[0] !== null && this.fileNamePredicts(last2[0], post.fileName)) || (last2[1] !== null && this.fileNamePredicts(last2[1], post.fileName))) {
  206. if (this.debug || (this.debugAfterDate && this.debugAfterDate < post.date)) {
  207. console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} merged (file name) ${postUser.id} with ${user.id} (author: ${this.author.id})`);
  208. }
  209. var mergedUser = this.union(user, postUser);
  210. last2Files.delete(user.parent != user ? user : postUser);
  211. last2Files.get(mergedUser).shift();
  212. last2Files.get(mergedUser).push(postFileName);
  213. if (this.find(this.author) == mergedUser) {
  214. this.author = mergedUser;
  215. }
  216. }
  217. });
  218. });
  219. return true;
  220. }
  221.  
  222. getUserPostAndFileCounts(posts, users, questFixes) {
  223. for (var userID in users) {
  224. users[userID].postCount = 0;
  225. users[userID].fileCount = 0;
  226. }
  227. posts.forEach(post => {
  228. var user = this.decidePostUser(post, users, questFixes);
  229. user.postCount++;
  230. if (post.fileName || post.activeContent) {
  231. user.fileCount++;
  232. }
  233. });
  234. }
  235.  
  236. fileNamePredicts(fileName1, fileName2) {
  237. var match1 = fileName1.match(this.regex.lastNumber);
  238. var match2 = fileName2.match(this.regex.lastNumber);
  239. if (!match1 || !match2) {
  240. return false;
  241. }
  242. var indexDifference = match2.index - match1.index;
  243. if (indexDifference > 1 || indexDifference < -1) {
  244. return false;
  245. }
  246. var numberDifference = parseInt(match2[1]) - parseInt(match1[1]);
  247. if (numberDifference !== 2 && numberDifference !== 1) {
  248. return false;
  249. }
  250. var name1 = fileName1.replace(this.regex.lastNumber, "");
  251. var name2 = fileName2.replace(this.regex.lastNumber, "");
  252. return this.stringsAreSimilar(name1, name2);
  253. }
  254.  
  255. stringsAreSimilar(string1, string2) {
  256. var lengthDiff = string1.length - string2.length;
  257. if (lengthDiff > 1 || lengthDiff < -1) {
  258. return false;
  259. }
  260. var s1 = lengthDiff > 0 ? string1 : string2;
  261. var s2 = lengthDiff > 0 ? string2 : string1;
  262. for (var i = 0, j = 0, diff = 0; i < s1.length; i++, j++) {
  263. if (s1[i] !== s2[j]) {
  264. diff++;
  265. if (diff === 2) {
  266. return false;
  267. }
  268. if (lengthDiff !== 0) {
  269. j--;
  270. }
  271. }
  272. }
  273. return true;
  274. }
  275.  
  276. mergeMajorityFilePoster(posts, users, questFixes) {
  277. if (this.author.fileCount > this.totalFiles / 2) {
  278. return;
  279. }
  280. for (var userID in users) {
  281. if (users[userID].fileCount >= this.totalFiles / 2 && users[userID] != this.author) {
  282. if (this.debug || (this.debugAfterDate && this.debugAfterDate < posts[posts.length - 1].date)) {
  283. console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html merged majority file poster ${users[userID].id} ${(100 * users[userID].fileCount / this.totalFiles).toFixed(1)}%`);
  284. }
  285. var parent = this.union(this.author, users[userID]);
  286. var child = users[userID].parent != users[userID] ? users[userID] : this.author;
  287. parent.fileCount += child.fileCount;
  288. parent.postCount += child.postCount;
  289. this.author = parent;
  290. return;
  291. }
  292. }
  293. }
  294.  
  295. mergeCommonFilePosters(posts, users, questFixes) {
  296. var merged = [];
  297. var filteredUsers = Object.values(users).filter(user => user.parent == user && user.fileCount >= 3 && user.fileCount / user.postCount > 0.5 && user != this.author);
  298. var usersSet = new Set(filteredUsers);
  299. posts.forEach(post => {
  300. if ((post.fileName || post.activeContent) && !questFixes.wrongImageUpdates[post.postID] && this.isTextPostAnUpdate(post)) {
  301. for (var user of usersSet) {
  302. if (this.find(users[post.userID]) == user) {
  303. if (this.debug || (this.debugAfterDate && this.debugAfterDate < post.date)) {
  304. console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html new common poster ${users[post.userID].id}`);
  305. }
  306. var parent = this.union(this.author, user);
  307. var child = user.parent != user ? user : this.author;
  308. parent.fileCount += child.fileCount;
  309. parent.postCount += child.postCount;
  310. this.author = parent;
  311. usersSet.delete(user);
  312. break;
  313. }
  314. }
  315. }
  316. });
  317. }
  318.  
  319. setPostUsers(posts, users, questFixes) {
  320. var postUsers = new Map();
  321. posts.forEach(post => {
  322. post.user = this.decidePostUser(post, users, questFixes);
  323. postUsers.set(post.postID, post.user);
  324. });
  325. return postUsers;
  326. }
  327.  
  328. decidePostUser(post, users, questFixes) {
  329. var user = this.find(users[post.userID]);
  330. if (post.userName) {
  331. if (questFixes.ignoreTextPosts[post.userName]) { //choose to the one that isn't the author
  332. if (user == this.author) {
  333. user = this.find(users[post.userName]);
  334. }
  335. }
  336. else if (this.find(users[post.userName]) == this.author) { //choose the one that is the author
  337. user = this.author;
  338. }
  339. }
  340. return user;
  341. }
  342.  
  343. getFinalPostTypes(posts, questFixes) {
  344. // Updates are posts made by the author and, in case of image quests, author posts that contain files or icons
  345. var postTypes = new Map();
  346. posts.forEach(post => {
  347. var postType = PostType.SUGGESTION;
  348. if (post.user == this.author) {
  349. if (post.fileName || post.activeContent) { //image post
  350. if (!questFixes.wrongImageUpdates[post.postID]) {
  351. postType = PostType.UPDATE;
  352. }
  353. else if (!questFixes.ignoreTextPosts[post.userID] && !questFixes.ignoreTextPosts[post.userName]) {
  354. postType = PostType.AUTHORCOMMENT;
  355. }
  356. }
  357. else if (!questFixes.ignoreTextPosts[post.userID] && !questFixes.ignoreTextPosts[post.userName]) { //text post
  358. if (!questFixes.wrongTextUpdates[post.postID] && (!this.imageQuest || this.isTextPostAnUpdate(post))) {
  359. postType = PostType.UPDATE;
  360. }
  361. else {
  362. postType = PostType.AUTHORCOMMENT;
  363. }
  364. }
  365. if (questFixes.missedTextUpdates[post.postID]) {
  366. postType = PostType.UPDATE;
  367. }
  368. }
  369. if (this.debugAfterDate && this.debugAfterDate < post.date) {
  370. if (postType == PostType.SUGGESTION && post.fileName) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new non-update`);
  371. if (postType == PostType.AUTHORCOMMENT) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new author comment`);
  372. if (postType == PostType.UPDATE && this.imageQuest && !post.fileName && !post.activeContent) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new text update`);
  373. }
  374. postTypes.set(post.postID, postType);
  375. });
  376. return postTypes;
  377. }
  378.  
  379. getPostUsers(posts) {
  380. var postUsers = new Map();
  381. posts.forEach(post => { postUsers.set(post.postID, post.user); });
  382. return postUsers;
  383. }
  384.  
  385. isTextPostAnUpdate(post) {
  386. if (post.textUpdate === undefined) {
  387. post.textUpdate = this.regex.fraction.test(post.subject) || this.containsQuotes(post.contentElement);
  388. }
  389. return post.textUpdate;
  390. }
  391.  
  392. containsQuotes(contentElement) {
  393. //extract post's text, but ignore text inside spoilers, links, dice rolls or any sort of brackets
  394. var filteredContentText = "";
  395. contentElement.childNodes.forEach(node => {
  396. if (node.className !== "spoiler" && node.nodeName != "A" && (node.nodeName != "B" || !this.regex.diceRoll.test(node.textContent))) {
  397. filteredContentText += node.textContent;
  398. }
  399. });
  400. filteredContentText = filteredContentText.replace(this.regex.bracketedTexts, "").trim();
  401. //if the post contains dialogue, then it's likely to be an update
  402. var quotedTexts = filteredContentText.match(this.regex.quotedTexts) || [];
  403. for (let q of quotedTexts) {
  404. if (this.regex.endsWithPunctuation.test(q)) {
  405. return true;
  406. }
  407. }
  408. return false;
  409. }
  410.  
  411. makeSet(id) {
  412. var node = { id: id, children: [] };
  413. node.parent = node;
  414. return node;
  415. }
  416.  
  417. find(node) { //find with path halving
  418. while (node.parent != node) {
  419. var curr = node;
  420. node = node.parent;
  421. curr.parent = node.parent;
  422. }
  423. return node;
  424. }
  425.  
  426. union(node1, node2) {
  427. var node1root = this.find(node1);
  428. var node2root = this.find(node2);
  429. if (node1root == node2root) {
  430. return node1root;
  431. }
  432. node2root.parent = node1root;
  433. node1root.children.push(node2root); //having a list of children isn't a part of Union-Find, but it makes debugging much easier
  434. node2root.children.forEach(child => node1root.children.push(child));
  435. return node1root;
  436. }
  437.  
  438. static getRegexes() {
  439. if (!this.regex) { //cache as a static class property
  440. this.regex = {
  441. fileExtension: new RegExp("[.][^.]+$"), //finds ".png" in "image.png"
  442. lastNumber: new RegExp("([0-9]+)(?=[^0-9]*$)"), //finds "50" in "image50.png"
  443. fraction: new RegExp("[0-9][ ]*/[ ]*[0-9]"), //finds "1/4" in "Update 1/4"
  444. diceRoll: new RegExp("^rolled [0-9].* = [0-9]+$"), //finds "rolled 10, 20 = 30"
  445. quotedTexts: new RegExp("[\"“”][^\"“”]*[\"“”]","gu"), //finds text inside quotes
  446. endsWithPunctuation: new RegExp("[.,!?][ ]*[\"“”]$"), //finds if a quote ends with a punctuation
  447. bracketedTexts: new RegExp("(\\([^)]*\\))|(\\[[^\\]]*\\])|(\\{[^}]*\\})|(<[^>]*>)", "gu"), //finds text within various kinds of brackets... looks funny
  448. canonID: new RegExp("^[0-9a-f]{6}$")
  449. };
  450. }
  451. return this.regex;
  452. }
  453.  
  454. getFixes(threadID) {
  455. var fixes = UpdateAnalyzer.getAllFixes()[threadID] || {};
  456. //convert array values to lower case and then into object properties for faster access
  457. for (let prop of [ "missedAuthors", "missedTextUpdates", "wrongTextUpdates", "wrongImageUpdates", "ignoreTextPosts" ]) {
  458. if (!fixes[prop]) {
  459. fixes[prop] = { };
  460. }
  461. else if (Array.isArray(fixes[prop])) { //can't use Array.reduce() because tgchan's js library protoaculous destroyed it
  462. fixes[prop] = fixes[prop].reduceRight((acc, el) => { if (!el.startsWith("!")) el = el.toLowerCase(); acc[el] = true; return acc; }, { });
  463. }
  464. }
  465. return fixes;
  466. }
  467.  
  468. // Manual fixes. In some cases it's simply impossible (impractical) to automatically determine which posts are updates. So we fix those rare cases manually.
  469. // list last updated on:
  470. // 2019/08/10
  471.  
  472. //missedAuthors: User IDs which should be linked to the author. Either because the automation failed, or the quest has guest authors / is a collaboration. Guest authors also usually need an entry under ignoreTextPosts.
  473. //ignoreTextPosts: User IDs of which text posts should not be set as author comments. It happens when a suggester shares an ID with the author and this suggester makes a text post. Or if the guest authors make suggestions.
  474. //(An empty ignoreTextPosts string matches posts with an empty/default poster name)
  475. //missedImageUpdates: Actually, no such fixes exist. All missed image update posts are added through adding author IDs to missedAuthors.
  476. //missedTextUpdates: Post IDs of text-only posts which are not author comments, but quest updates. It happens when authors make text updates in image quests. Or forget to attach an image to the update post.
  477. //wrongImageUpdates: Post IDs of image posts which are not quest updates. It happens when a suggester shares an ID with the author(s) and this suggester makes an image post. Or a guest author posts a non-update image post.
  478. //wrongTextUpdates: Post IDs of text-only posts which were misidentified as updates. It happens when an author comment contains a valid quote and the script accidentally thinks some dialogue is going on.
  479. //imageQuest: Forcefully set quest type. It happens when the automatically-determined quest type is incorrect. Either because of too many image updates in a text quest, or text updates in an image quest.
  480. //(Also, if most of the author's text posts in an image quest are updates, then it's sometimes simpler to set the quest as a text quest, rather than picking them out one by one.)
  481. static getAllFixes() {
  482. if (!this.allFixes) {
  483. this.allFixes = { //cache as a static class property
  484. 12: { missedAuthors: [ "!g9Qfmdqho2" ] },
  485. 26: { ignoreTextPosts: [ "Coriell", "!DHEj4YTg6g" ] },
  486. 101: { wrongTextUpdates: [ "442" ] },
  487. 171: { wrongTextUpdates: [ "1402" ] },
  488. 504: { missedTextUpdates: [ "515", "597", "654", "1139", "1163", "1180", "7994", "9951" ] },
  489. 998: { ignoreTextPosts: [ "" ] },
  490. 1292: { missedAuthors: [ "Chaptermaster II" ], missedTextUpdates: [ "1311", "1315", "1318" ], ignoreTextPosts: [ "" ] },
  491. 1702: { wrongImageUpdates: [ "2829" ] },
  492. 3090: { ignoreTextPosts: [ "", "!94Ud9yTfxQ", "Glaive" ], wrongImageUpdates: [ "3511", "3574", "3588", "3591", "3603", "3612" ] },
  493. 4602: { missedTextUpdates: [ "4630", "6375" ] },
  494. 7173: { missedTextUpdates: [ "8515", "10326" ] },
  495. 8906: { missedTextUpdates: [ "9002", "9009" ] },
  496. 9190: { missedAuthors: [ "!OeZ2B20kbk" ], missedTextUpdates: [ "26073" ] },
  497. 13595: { wrongTextUpdates: [ "18058" ] },
  498. 16114: { missedTextUpdates: [ "20647" ] },
  499. 17833: { ignoreTextPosts: [ "!swQABHZA/E" ] },
  500. 19308: { missedTextUpdates: [ "19425", "19600", "19912" ] },
  501. 19622: { wrongImageUpdates: [ "30710", "30719", "30732", "30765" ] },
  502. 19932: { missedTextUpdates: [ "20038", "20094", "20173", "20252" ] },
  503. 20501: { ignoreTextPosts: [ "bd2eec" ] },
  504. 21601: { missedTextUpdates: [ "21629", "21639" ] },
  505. 21853: { missedTextUpdates: [ "21892", "21898", "21925", "22261", "22266", "22710", "23308", "23321", "23862", "23864", "23900", "24206", "25479", "25497", "25943", "26453", "26787", "26799",
  506. "26807", "26929", "27328", "27392", "27648", "27766", "27809", "29107", "29145" ] },
  507. 22208: { missedAuthors: [ "fb5d8e" ] },
  508. 24530: { wrongImageUpdates: [ "25023" ] },
  509. 25354: { imageQuest: false},
  510. 26933: { missedTextUpdates: [ "26935", "26955", "26962", "26967", "26987", "27015", "28998" ] },
  511. 29636: { missedTextUpdates: [ "29696", "29914", "30025", "30911" ], wrongImageUpdates: [ "30973", "32955", "33107" ] },
  512. 30350: { imageQuest: false, wrongTextUpdates: [ "30595", "32354", "33704" ] },
  513. 30357: { missedTextUpdates: [ "30470", "30486", "30490", "30512", "33512" ] },
  514. 33329: { wrongTextUpdates: [ "43894" ] },
  515. 37304: { ignoreTextPosts: [ "", "GREEN", "!!x2ZmLjZmyu", "Adept", "Cruxador", "!ifOCf11HXk" ] },
  516. 37954: { missedTextUpdates: [ "41649" ] },
  517. 38276: { ignoreTextPosts: [ "!ifOCf11HXk" ] },
  518. 41510: { missedTextUpdates: [ "41550", "41746" ] },
  519. 44240: { missedTextUpdates: [ "44324", "45768", "45770", "48680", "48687" ] },
  520. 45522: { missedTextUpdates: [ "55885" ] },
  521. 45986: { missedTextUpdates: [ "45994", "46019" ] },
  522. 49306: { missedTextUpdates: [ "54246" ] },
  523. 49400: { ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  524. 49937: { missedTextUpdates: [ "52386" ] },
  525. 53129: { wrongTextUpdates: [ "53505" ] },
  526. 53585: { missedAuthors: [ "b1e366", "aba0a3", "18212a", "6756f8", "f98e0b", "1c48f4", "f4963f", "45afb1", "b94893", "135d9a" ], ignoreTextPosts: [ "", "!7BHo7QtR6I", "Test Pattern", "Rowan", "Insomnia", "!!L1ZwWyZzZ5" ] },
  527. 54766: { missedAuthors: [ "e16ca8" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  528. 55639: { wrongImageUpdates: [ "56711", "56345", "56379", "56637" ] },
  529. 56194: { wrongTextUpdates: [ "61608" ] },
  530. 59263: { missedTextUpdates: [ "64631" ] },
  531. 62091: { imageQuest: true},
  532. 65742: { missedTextUpdates: [ "66329", "66392", "67033", "67168" ] },
  533. 67058: { missedTextUpdates: [ "67191", "67685" ] },
  534. 68065: { missedAuthors: [ "7452df", "1d8589" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  535. 70887: { missedAuthors: [ "e53955", "7c9cdd", "2084ff", "064d19", "51efff", "d3c8d2" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  536. 72794: { wrongTextUpdates: [ "76740" ] },
  537. 74474: { missedAuthors: [ "309964" ] },
  538. 75425: { missedTextUpdates: [ "75450", "75463", "75464", "75472", "75490", "75505", "77245" ] },
  539. 75763: { missedAuthors: [ "068b0e" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] },
  540. 76892: { missedTextUpdates: [ "86875", "86884", "87047", "88315" ] },
  541. 79146: { missedAuthors: [ "4a3269" ] },
  542. 79654: { missedTextUpdates: [ "83463", "83529" ] },
  543. 79782: { missedTextUpdates: [ "79975", "80045" ] },
  544. 82970: { missedTextUpdates: [ "84734" ] },
  545. 83325: { missedAuthors: [ "076064" ] },
  546. 84134: { imageQuest: false},
  547. 85235: { missedTextUpdates: [ "85257", "85282", "113215", "114739", "151976", "152022", "159250" ] },
  548. 88264: { missedAuthors: [ "3fec76", "714b9c" ] },
  549. 92605: { ignoreTextPosts: [ "" ] },
  550. 94645: { missedTextUpdates: [ "97352" ] },
  551. 95242: { missedTextUpdates: [ "95263" ] },
  552. 96023: { missedTextUpdates: [ "96242" ] },
  553. 96466: { ignoreTextPosts: [ "Reverie" ] },
  554. 96481: { imageQuest: true},
  555. 97014: { missedTextUpdates: [ "97061", "97404", "97915", "98124", "98283", "98344", "98371", "98974", "98976", "98978", "99040", "99674", "99684" ] },
  556. 99095: { wrongImageUpdates: [ "111452" ] },
  557. 99132: { ignoreTextPosts: [ "" ] },
  558. 100346: { missedTextUpdates: [ "100626", "100690", "100743", "100747", "101143", "101199", "101235", "101239" ] },
  559. 101388: { ignoreTextPosts: [ "Glaive" ] },
  560. 102433: { missedTextUpdates: [ "102519", "102559", "102758" ] },
  561. 102899: { missedTextUpdates: [ "102903" ] },
  562. 103435: { missedTextUpdates: [ "104279", "105950" ] },
  563. 103850: { ignoreTextPosts: [ "" ] },
  564. 106656: { wrongTextUpdates: [ "115606" ] },
  565. 107789: { missedTextUpdates: [ "107810", "107849", "107899" ] },
  566. 108599: { wrongImageUpdates: [ "171382", "172922", "174091", "180752", "180758" ] },
  567. 108805: { wrongImageUpdates: [ "110203" ] },
  568. 109071: { missedTextUpdates: [ "109417" ] },
  569. 112133: { missedTextUpdates: [ "134867" ] },
  570. 112414: { missedTextUpdates: [ "112455" ] },
  571. 113768: { missedAuthors: [ "e9a4f7" ] },
  572. 114133: { ignoreTextPosts: [ "" ] },
  573. 115831: { missedTextUpdates: [ "115862" ] },
  574. 119431: { ignoreTextPosts: [ "" ] },
  575. 120384: { missedAuthors: [ "233aab" ] },
  576. 126204: { imageQuest: true, missedTextUpdates: [ "127069", "127089", "161046", "161060", "161563" ] },
  577. 126248: { missedTextUpdates: [ "193064" ] },
  578. 128706: { missedAuthors: [ "2e2f06", "21b50e", "e0478c", "9c87f6", "931351", "e294f1", "749d64", "f3254a" ] },
  579. 131255: { missedTextUpdates: [ "151218" ] },
  580. 137683: { missedTextUpdates: [ "137723" ] },
  581. 139086: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  582. 139513: { missedTextUpdates: [ "139560" ] },
  583. 141257: { missedTextUpdates: [ "141263", "141290", "141513", "146287" ], ignoreTextPosts: [ "" ], wrongImageUpdates: [ "141265" ] },
  584. 146112: { missedAuthors: [ "//_emily" ] },
  585. 153225: { missedTextUpdates: [ "153615", "153875" ] },
  586. 155665: { missedTextUpdates: [ "155670", "155684", "155740" ] },
  587. 156257: { missedTextUpdates: [ "156956" ] },
  588. 157277: { missedAuthors: [ "23c8f1", "8bb533" ] },
  589. 161117: { missedTextUpdates: [ "167255", "168000" ] },
  590. 162089: { missedTextUpdates: [ "167940" ] },
  591. 164793: { missedAuthors: [ "e973f4" ], ignoreTextPosts: [ "!TEEDashxDA" ] },
  592. 165537: { missedAuthors: [ "a9f6ce" ] },
  593. 173621: { ignoreTextPosts: [ "" ] },
  594. 174398: { missedAuthors: [ "bf0d4e", "158c5c" ] },
  595. 176965: { missedTextUpdates: [ "177012" ] },
  596. 177281: { missedTextUpdates: [ "178846" ] },
  597. 181790: { ignoreTextPosts: [ "Mister Brush" ], wrongImageUpdates: [ "182280" ] },
  598. 183194: { ignoreTextPosts: [ "!CRITTerXzI" ], wrongImageUpdates: [ "183207" ] },
  599. 183637: { imageQuest: false, wrongTextUpdates: [ "183736" ] },
  600. 185345: { wrongTextUpdates: [ "185347" ] },
  601. 185579: { missedTextUpdates: [ "188091", "188697", "188731", "188748", "190868" ] },
  602. 186709: { missedTextUpdates: [ "186735" ] },
  603. 188253: { missedTextUpdates: [ "215980", "215984", "222136" ] },
  604. 188571: { missedTextUpdates: [ "188633" ] },
  605. 188970: { ignoreTextPosts: [ "" ] },
  606. 191328: { missedAuthors: [ "f54a9c", "862cf6", "af7d90", "4c1052", "e75bed", "09e145" ] },
  607. 191976: { missedAuthors: [ "20fc85" ] },
  608. 192879: { missedTextUpdates: [ "193009" ] },
  609. 193934: { missedTextUpdates: [ "212768" ] },
  610. 196310: { missedTextUpdates: [ "196401" ] },
  611. 196517: { missedTextUpdates: [ "196733" ] },
  612. 198458: { missedTextUpdates: [ "198505", "198601", "199570" ] },
  613. 200054: { missedAuthors: [ "a4b4e3" ] },
  614. 201427: { missedTextUpdates: [ "201467", "201844" ] },
  615. 203072: { missedTextUpdates: [ "203082", "203100", "206309", "207033", "208766" ] },
  616. 206945: { missedTextUpdates: [ "206950" ] },
  617. 207011: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  618. 207296: { missedTextUpdates: [ "214551" ] },
  619. 207756: { missedTextUpdates: [ "208926" ] },
  620. 209334: { missedTextUpdates: [ "209941" ] },
  621. 210613: { missedTextUpdates: [ "215711", "220853" ] },
  622. 210928: { missedTextUpdates: [ "215900" ] },
  623. 211320: { ignoreTextPosts: [ "Kindling", "Bahu" ], wrongImageUpdates: [ "211587", "215436" ] },
  624. 212584: { missedAuthors: [ "40a8d3" ] },
  625. 212915: { missedTextUpdates: [ "229550" ] },
  626. 217193: { missedAuthors: [ "7f1ecd", "c00244", "7c97d9", "8c0848", "491db1", "c2c011", "e15f89",
  627. "e31d52", "3ce5b4", "c1f2ce", "5f0943", "1dc978", "d65652", "446ab5", "f906a7", "dad664", "231806" ] },
  628. 217269: { imageQuest: false, wrongTextUpdates: [ "217860", "219314" ] },
  629. 218385: { missedAuthors: [ "812dcf" ] },
  630. 220049: { ignoreTextPosts: [ "Slinkoboy" ], wrongImageUpdates: [ "228035", "337790" ] },
  631. 222777: { imageQuest: false},
  632. 224095: { missedTextUpdates: [ "224196", "224300", "224620", "244476" ] },
  633. 233213: { missedTextUpdates: [ "233498" ], ignoreTextPosts: [ "Bahu" ] },
  634. 234437: { missedTextUpdates: [ "234657" ] },
  635. 237125: { missedTextUpdates: [ "237192" ] },
  636. 237665: { imageQuest: true, ignoreTextPosts: [ "" ] },
  637. 238281: { ignoreTextPosts: [ "TK" ] },
  638. 238993: { missedTextUpdates: [ "239018", "239028", "239094" ] },
  639. 240824: { imageQuest: false},
  640. 241467: { missedTextUpdates: [ "241709" ] },
  641. 242200: { missedTextUpdates: [ "246465", "246473", "246513" ] },
  642. 242657: { missedAuthors: [ "2563d4" ] },
  643. 244225: { missedTextUpdates: [ "245099", "245195", "245201" ] },
  644. 244557: { missedTextUpdates: [ "244561" ], ignoreTextPosts: [ "" ] },
  645. 244830: { missedAuthors: [ "e33093" ] },
  646. 247108: { ignoreTextPosts: [ "Bahu" ], wrongImageUpdates: [ "258883", "265446" ] },
  647. 247714: { missedTextUpdates: [ "247852" ] },
  648. 248067: { ignoreTextPosts: [ "" ] },
  649. 248856: { ignoreTextPosts: [ "" ] },
  650. 248880: { imageQuest: true, ignoreTextPosts: [ "", "!qkgg.NzvRY", "!!EyA2IwLwVl", "!I10GFLsZCw", "!k6uRjGDgAQ", "Seven01a19" ] },
  651. 251909: { missedTextUpdates: [ "255400" ] },
  652. 252195: { missedTextUpdates: [ "260890" ] },
  653. 252944: { missedAuthors: [ "Rizzie" ], ignoreTextPosts: [ "", "!!EyA2IwLwVl", "Seven01a19" ] },
  654. 256339: { missedTextUpdates: [ "256359", "256379", "256404", "256440" ] },
  655. 257726: { missedAuthors: [ "917cac" ] },
  656. 258304: { missedTextUpdates: [ "269087" ] },
  657. 261572: { imageQuest: false},
  658. 261837: { missedAuthors: [ "14149d" ] },
  659. 262128: { missedTextUpdates: [ "262166", "262219", "262455", "262500" ] },
  660. 262574: { missedAuthors: [ "b7798b", "0b5a64", "687829", "446f39", "cc1ccd", "9d3d72", "72d5e4", "932db9", "4d7cb4", "9f327a", "940ab2", "a660d0" ], ignoreTextPosts: [ "" ] },
  661. 263831: { imageQuest: false, wrongTextUpdates: [ "264063", "264716", "265111", "268733", "269012", "270598", "271254", "271852", "271855", "274776", "275128", "280425", "280812", "282417", "284354", "291231", "300074", "305150" ] },
  662. 265656: { ignoreTextPosts: [ "Glaive17" ] },
  663. 266542: { missedAuthors: [ "MidKnight", "c2c011", "f5e4b4", "e973f4", "6547ec" ], ignoreTextPosts: [ "", "!TEEDashxDA", "Not Cirr", "Ñ" ] },
  664. 267348: { ignoreTextPosts: [ "" ] },
  665. 269735: { ignoreTextPosts: [ "---" ] },
  666. 270556: { ignoreTextPosts: [ "Bahu" ], wrongImageUpdates: [ "276022" ] },
  667. 273047: { missedAuthors: [ "db463d", "16f0be", "77df62", "b6733e", "d171a3", "3a95e1", "21d450" ] },
  668. 274088: { missedAuthors: [ "4b0cf3" ], missedTextUpdates: [ "294418" ], ignoreTextPosts: [ "" ] },
  669. 274466: { missedAuthors: [ "c9efe3" ] },
  670. 276562: { missedTextUpdates: [ "277108" ] },
  671. 277371: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  672. 278168: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  673. 280381: { ignoreTextPosts: [ "!7BHo7QtR6I" ] },
  674. 280985: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  675. 283246: { imageQuest: false},
  676. 285210: { ignoreTextPosts: [ "", "Weaver" ] },
  677. 287296: { ignoreTextPosts: [ "", "Asplosionz" ] },
  678. 287815: { missedAuthors: [ "Ñ" ] },
  679. 288346: { missedAuthors: [ "383006", "bf1e7e" ], ignoreTextPosts: [ "383006", "bf1e7e" ] },
  680. 289254: { imageQuest: false},
  681. 292033: { wrongTextUpdates: [ "295088" ] },
  682. 293532: { ignoreTextPosts: [ "" ] },
  683. 294351: { ignoreTextPosts: [ "Weaver" ] },
  684. 295374: { ignoreTextPosts: [ "TK" ] },
  685. 295832: { missedAuthors: [ "ac22cd", "7afbc4", "6f11ff" ], missedTextUpdates: [ "313940" ] },
  686. 295949: { missedTextUpdates: [ "296256", "297926", "298549" ] },
  687. 298133: { missedTextUpdates: [ "298187" ] },
  688. 298860: { imageQuest: true, missedTextUpdates: [ "298871", "298877", "298880", "298908" ] },
  689. 299352: { imageQuest: true, missedTextUpdates: [ "299375", "299627", "303689" ] },
  690. 300694: { ignoreTextPosts: [ "TK" ] },
  691. 300751: { missedTextUpdates: [ "316287" ] },
  692. 303859: { ignoreTextPosts: [ "" ] },
  693. 308257: { missedTextUpdates: [ "314653" ] },
  694. 309753: { missedTextUpdates: [ "309864", "309963", "310292", "310944", "310987", "311202", "311219", "311548" ] },
  695. 310586: { missedTextUpdates: [ "310945", "312747", "313144" ] },
  696. 311021: { missedAuthors: [ "049dfa", "f2a6f9" ] },
  697. 312418: { missedTextUpdates: [ "312786", "312790", "312792", "312984", "313185" ] },
  698. 314825: { ignoreTextPosts: [ "TK" ] },
  699. 314940: { missedTextUpdates: [ "314986", "315198", "329923" ] },
  700. 318478: { ignoreTextPosts: [ "Toxoglossa" ] },
  701. 319491: { ignoreTextPosts: [ "Bahu" ] },
  702. 323481: { missedTextUpdates: [ "323843", "324125", "324574" ] },
  703. 323589: { missedTextUpdates: [ "329499" ] },
  704. 327468: { missedTextUpdates: [ "327480", "337008" ] },
  705. 337661: { ignoreTextPosts: [ "", "hisgooddog" ] },
  706. 338579: { ignoreTextPosts: [ "", "Zealo8", "Ñ" ] },
  707. 343078: { wrongImageUpdates: [ "343219" ] },
  708. 343668: { missedTextUpdates: [ "343671" ] },
  709. 348635: { ignoreTextPosts: [ "" ] },
  710. 351064: { missedTextUpdates: [ "351634", "353263", "355326", "356289" ] },
  711. 351264: { missedTextUpdates: [ "353077" ] },
  712. 354201: { imageQuest: true, missedTextUpdates: [ "354340" ] },
  713. 355404: { ignoreTextPosts: [ "Bahu" ] },
  714. 356715: { missedTextUpdates: [ "356722" ] },
  715. 357723: { missedAuthors: [ "7bad01" ], ignoreTextPosts: [ "", "SoqWizard" ] },
  716. 359879: { imageQuest: false},
  717. 359931: { missedAuthors: [ "Dasaki", "Rynh", "Kinasa", "178c80" ], ignoreTextPosts: [ "", "Gnoll", "Lost Planet", "Dasaki", "Slinkoboy" ] },
  718. 360617: { missedAuthors: [ "7a7217" ] },
  719. 363529: { imageQuest: true, ignoreTextPosts: [ "Tenyoken" ] },
  720. 365082: { missedTextUpdates: [ "381411", "382388" ] },
  721. 366944: { missedTextUpdates: [ "367897" ] },
  722. 367145: { wrongTextUpdates: [ "367887" ] },
  723. 367824: { missedTextUpdates: [ "367841", "367858", "367948" ] },
  724. 375293: { ignoreTextPosts: [ "Bahu" ] },
  725. 382864: { ignoreTextPosts: [ "FlynnMerk" ] },
  726. 387602: { ignoreTextPosts: [ "!a1..dIzWW2" ], wrongImageUpdates: [ "390207", "392018", "394748" ] },
  727. 388264: { ignoreTextPosts: [ "" ] },
  728. 392034: { missedAuthors: [ "046f13" ] },
  729. 392868: { missedAuthors: [ "e1359e" ] },
  730. 393082: { ignoreTextPosts: [ "" ] },
  731. 395700: { missedTextUpdates: [ "395701", "395758" ] },
  732. 395817: { ignoreTextPosts: [ "" ] },
  733. 397819: { ignoreTextPosts: [ "Bahu", "K-Dogg" ], wrongImageUpdates: [ "398064" ] },
  734. 400842: { missedAuthors: [ "b0d466" ], ignoreTextPosts: [ "", "!a1..dIzWW2" ], wrongImageUpdates: [ "412172", "412197" ] },
  735. 403418: { missedAuthors: [ "02cbc6" ] },
  736. 404177: { missedTextUpdates: [ "404633" ] },
  737. 409356: { missedTextUpdates: [ "480664", "485493" ], wrongTextUpdates: [ "492824" ] },
  738. 410618: { ignoreTextPosts: [ "kathryn" ], wrongImageUpdates: [ "417836" ] },
  739. 412463: { ignoreTextPosts: [ "" ] },
  740. 413494: { ignoreTextPosts: [ "Bahu" ] },
  741. 420600: { imageQuest: false},
  742. 421477: { imageQuest: false},
  743. 422052: { missedAuthors: [ "!a1..dIzWW2" ] },
  744. 422087: { ignoreTextPosts: [ "Caz" ] },
  745. 422856: { ignoreTextPosts: [ "", "???" ] },
  746. 424198: { missedAuthors: [ "067a04" ], ignoreTextPosts: [ "!a1..dIzWW2" ] },
  747. 425677: { missedTextUpdates: [ "425893", "426741", "431953" ] },
  748. 426019: { ignoreTextPosts: [ "Taskuhecate" ] },
  749. 427135: { ignoreTextPosts: [ "!7BHo7QtR6I" ] },
  750. 427676: { ignoreTextPosts: [ "FRACTAL" ] },
  751. 428027: { ignoreTextPosts: [ "notrottel", "Bahu", "!a1..dIzWW2", "Trout", "Larro", "", "cuoqet" ], wrongImageUpdates: [ "428285", "498295" ] },
  752. 430036: { missedTextUpdates: [ "430062", "430182", "430416" ], ignoreTextPosts: [ "" ] },
  753. 431445: { imageQuest: false, missedAuthors: [ "efbb86" ] },
  754. 435947: { missedTextUpdates: [ "436059" ] },
  755. 437675: { wrongTextUpdates: [ "445770", "449255", "480401" ] },
  756. 437768: { missedTextUpdates: [ "446536" ] },
  757. 438515: { ignoreTextPosts: [ "TK" ] },
  758. 438670: { ignoreTextPosts: [ "" ] },
  759. 441226: { missedAuthors: [ "6a1ec2", "99090a", "7f2d33" ], wrongImageUpdates: [ "441260" ] },
  760. 441745: { missedTextUpdates: [ "443831" ] },
  761. 447830: { imageQuest: false, missedAuthors: [ "fc985a", "f8b208" ], wrongTextUpdates: [ "448476", "450379", "452161" ] },
  762. 448900: { missedAuthors: [ "0c2256" ] },
  763. 449505: { wrongTextUpdates: [ "450499" ] },
  764. 450563: { missedAuthors: [ "!!AwZwHkBGWx", "Oregano" ], ignoreTextPosts: [ "", "chirps", "!!AwZwHkBGWx", "!!AwZwHkBGWx", "Ham" ] },
  765. 452871: { missedAuthors: [ "General Q. Waterbuffalo", "!cZFAmericA" ], missedTextUpdates: [ "456083" ] },
  766. 453480: { ignoreTextPosts: [ "TK" ], wrongImageUpdates: [ "474233" ] },
  767. 453978: { missedTextUpdates: [ "453986" ] },
  768. 454256: { missedTextUpdates: [ "474914", "474957" ] },
  769. 456185: { ignoreTextPosts: [ "TK" ], wrongTextUpdates: [ "472446" ], wrongImageUpdates: [ "592622" ] },
  770. 456798: { missedTextUpdates: [ "516303" ] },
  771. 458432: { missedAuthors: [ "259cce", "34cbef" ] },
  772. 463595: { missedTextUpdates: [ "463711", "465024", "465212", "465633", "467107", "467286" ], wrongTextUpdates: [ "463623" ] },
  773. 464038: { missedAuthors: [ "df885d", "8474cd" ] },
  774. 465919: { missedTextUpdates: [ "465921" ] },
  775. 469321: { missedTextUpdates: [ "469332" ] },
  776. 471304: { missedAuthors: [ "1766db" ] },
  777. 471394: { missedAuthors: [ "Cirr" ] },
  778. 476554: { ignoreTextPosts: [ "Fish is yum" ] },
  779. 478624: { missedAuthors: [ "88c9b2" ] },
  780. 479712: { ignoreTextPosts: [ "" ] },
  781. 481277: { missedTextUpdates: [ "481301", "482210" ], ignoreTextPosts: [ "Santova" ] },
  782. 481491: { missedTextUpdates: [ "481543", "481575", "484069" ], ignoreTextPosts: [ "Zach Leigh", "Santova", "Outaki Shiba" ] },
  783. 482391: { missedTextUpdates: [ "482501", "482838" ] },
  784. 482629: { missedTextUpdates: [ "484220", "484437" ], ignoreTextPosts: [ "Santova", "Tera Nospis" ] },
  785. 483108: { missedAuthors: [ "2de44c" ], missedTextUpdates: [ "483418", "483658" ], ignoreTextPosts: [ "Santova" ] },
  786. 484423: { missedTextUpdates: [ "484470", "486761", "488602" ], ignoreTextPosts: [ "Tera Nospis", "Zach Leigh" ] },
  787. 484606: { missedTextUpdates: [ "486773" ], ignoreTextPosts: [ "Zach Leigh" ] },
  788. 485964: { missedTextUpdates: [ "489145", "489760" ], ignoreTextPosts: [ "Tera Nospis", "Santova" ] },
  789. 489488: { missedTextUpdates: [ "490389" ] },
  790. 489694: { missedAuthors: [ "2c8bbe", "30a140", "8c4b01", "8fbeb2", "2b7d97", "17675d", "782175", "665fcd", "e91794", "52019c", "8ef0aa", "e493a6", "c847bc" ] },
  791. 489830: { missedAuthors: [ "9ee824", "8817a0", "d81bd3", "704658" ] },
  792. 490689: { ignoreTextPosts: [ "Santova" ] },
  793. 491171: { ignoreTextPosts: [ "Santova", "Zach Leigh", "Zack Leigh", "The Creator" ] },
  794. 491314: { missedTextUpdates: [ "491498" ], ignoreTextPosts: [ "" ] },
  795. 492511: { missedAuthors: [ "???" ] },
  796. 493099: { ignoreTextPosts: [ "Zach Leigh", "Santova" ] },
  797. 494015: { ignoreTextPosts: [ "Coda", "drgruff" ] },
  798. 496561: { ignoreTextPosts: [ "Santova", "DJ LaLonde", "Tera Nospis" ] },
  799. 498874: { ignoreTextPosts: [ "Santova" ] },
  800. 499607: { ignoreTextPosts: [ "Santova", "Tera Nospis" ] },
  801. 499980: { ignoreTextPosts: [ "Santova", "Tera Nospis", "DJ LaLonde" ] },
  802. 500015: { missedTextUpdates: [ "500020", "500029", "500274", "501462", "501464", "501809", "505421" ], ignoreTextPosts: [ "suggestion", "Chelz" ] },
  803. 502751: { ignoreTextPosts: [ "suggestion" ] },
  804. 503053: { missedAuthors: [ "!!WzMJSzZzWx", "Shopkeep", "CAI" ] },
  805. 505072: { missedTextUpdates: [ "565461" ] },
  806. 505569: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  807. 505633: { missedTextUpdates: [ "505694", "529582" ] },
  808. 505796: { ignoreTextPosts: [ "Mister-Saturn" ] },
  809. 506555: { ignoreTextPosts: [ "Tera Nospis", "Santova" ] },
  810. 507761: { ignoreTextPosts: [ "", "Rue" ] },
  811. 508294: { missedAuthors: [ "Lisila" ], missedTextUpdates: [ "508618", "508406" ] },
  812. 509510: { missedTextUpdates: [ "509810", "510805", "510812", "510943", "511042", "512430", "514731", "515963" ] },
  813. 510067: { missedTextUpdates: [ "510081" ] },
  814. 511816: { imageQuest: true, missedAuthors: [ "34cf7d" ], missedTextUpdates: [ "512608" ] },
  815. 512417: { ignoreTextPosts: [ "Uplifted" ] },
  816. 512501: { ignoreTextPosts: [ "" ] },
  817. 512569: { wrongImageUpdates: [ "512810" ] },
  818. 513727: { missedTextUpdates: [ "519251" ], ignoreTextPosts: [ "!mYSM8eo.ng" ] },
  819. 514174: { missedTextUpdates: [ "747164" ] },
  820. 515255: { ignoreTextPosts: [ "" ] },
  821. 516595: { imageQuest: true},
  822. 517144: { ignoreTextPosts: [ "" ] },
  823. 518737: { wrongTextUpdates: [ "521408", "522150", "522185", "522231", "535521" ] },
  824. 518843: { ignoreTextPosts: [ "" ] },
  825. 519463: { imageQuest: false},
  826. 521196: { missedTextUpdates: [ "524608" ] },
  827. 526472: { missedTextUpdates: [ "526524", "559848" ] },
  828. 527296: { ignoreTextPosts: [ "Zealo8" ] },
  829. 527546: { ignoreTextPosts: [ "suggestion" ] },
  830. 527753: { missedAuthors: [ "7672c3", "9d78a6", "cb43c1" ] },
  831. 528891: { ignoreTextPosts: [ "drgruff" ] },
  832. 530940: { missedAuthors: [ "2027bb", "feafa5", "0a3b00" ] },
  833. 533990: { missedTextUpdates: [ "537577" ] },
  834. 534197: { ignoreTextPosts: [ "Stella" ] },
  835. 535302: { ignoreTextPosts: [ "mermaid" ] },
  836. 535783: { ignoreTextPosts: [ "drgruff" ] },
  837. 536268: { missedTextUpdates: [ "536296", "538173" ], ignoreTextPosts: [ "Archivemod" ], wrongImageUpdates: [ "537996" ] },
  838. 537343: { missedTextUpdates: [ "539218" ] },
  839. 537647: { missedTextUpdates: [ "537683" ] },
  840. 537867: { missedAuthors: [ "369097" ] },
  841. 539831: { ignoreTextPosts: [ "" ] },
  842. 540147: { ignoreTextPosts: [ "drgruff" ] },
  843. 541026: { imageQuest: false},
  844. 543428: { missedTextUpdates: [ "545458" ] },
  845. 545071: { missedTextUpdates: [ "545081" ] },
  846. 545791: { ignoreTextPosts: [ "" ] },
  847. 545842: { missedTextUpdates: [ "550972" ] },
  848. 548052: { missedTextUpdates: [ "548172" ], ignoreTextPosts: [ "Lucid" ] },
  849. 548899: { missedTextUpdates: [ "548968", "549003" ] },
  850. 549394: { missedTextUpdates: [ "549403" ] },
  851. 553434: { missedTextUpdates: [ "553610", "553635", "553668", "554166" ] },
  852. 553711: { missedTextUpdates: [ "553722", "553728", "554190" ] },
  853. 553760: { missedTextUpdates: [ "554994", "555829", "556570", "556792", "556803", "556804" ] },
  854. 554694: { missedTextUpdates: [ "557011", "560544" ] },
  855. 556435: { missedAuthors: [ "Azathoth" ], missedTextUpdates: [ "607163" ], wrongTextUpdates: [ "561150" ] },
  856. 557051: { missedTextUpdates: [ "557246", "557260", "557599", "559586" ], wrongTextUpdates: [ "557517" ] },
  857. 557633: { imageQuest: true},
  858. 557854: { missedTextUpdates: [ "557910", "557915", "557972", "558082", "558447", "558501", "561834", "561836", "562289", "632102", "632481", "632509", "632471" ] },
  859. 562193: { ignoreTextPosts: [ "" ] },
  860. 563459: { missedTextUpdates: [ "563582" ] },
  861. 564852: { ignoreTextPosts: [ "Trout" ] },
  862. 564860: { missedTextUpdates: [ "565391" ] },
  863. 565909: { ignoreTextPosts: [ "" ] },
  864. 567119: { missedTextUpdates: [ "573494", "586375" ] },
  865. 567138: { missedAuthors: [ "4cf1b6" ] },
  866. 568248: { missedTextUpdates: [ "569818" ] },
  867. 568370: { ignoreTextPosts: [ "" ] },
  868. 568463: { missedTextUpdates: [ "568470", "568473" ] },
  869. 569225: { missedTextUpdates: [ "569289" ] },
  870. 573815: { wrongTextUpdates: [ "575792" ] },
  871. 578213: { missedTextUpdates: [ "578575" ] },
  872. 581741: { missedTextUpdates: [ "581746" ] },
  873. 582268: { missedTextUpdates: [ "587221" ] },
  874. 585201: { ignoreTextPosts: [ "", "Bahustard", "Siphon" ] },
  875. 586024: { ignoreTextPosts: [ "" ] },
  876. 587086: { missedTextUpdates: [ "587245", "587284", "587443", "587454" ] },
  877. 587562: { ignoreTextPosts: [ "Zealo8" ] },
  878. 588902: { missedTextUpdates: [ "589033" ] },
  879. 589725: { imageQuest: false},
  880. 590502: { ignoreTextPosts: [ "" ], wrongTextUpdates: [ "590506" ] },
  881. 590761: { missedTextUpdates: [ "590799" ], ignoreTextPosts: [ "" ] },
  882. 591527: { missedTextUpdates: [ "591547", "591845" ] },
  883. 592273: { imageQuest: false},
  884. 592625: { wrongTextUpdates: [ "730228" ] },
  885. 593047: { missedTextUpdates: [ "593065", "593067", "593068" ] },
  886. 593899: { ignoreTextPosts: [ "mermaid" ] },
  887. 595081: { ignoreTextPosts: [ "", "VoidWitchery" ] },
  888. 595265: { imageQuest: false, wrongTextUpdates: [ "596676", "596717", "621360", "621452", "621466", "621469", "621503" ] },
  889. 596262: { missedTextUpdates: [ "596291", "596611", "597910", "598043", "598145", "600718", "603311" ] },
  890. 596345: { ignoreTextPosts: [ "mermaid" ] },
  891. 596539: { missedTextUpdates: [ "596960", "596972", "596998", "597414", "614375", "614379", "614407", "616640", "668835", "668844", "668906", "668907", "668937", "668941", "669049", "669050",
  892. "669126", "671651" ], ignoreTextPosts: [ "pugbutt" ] },
  893. 598767: { ignoreTextPosts: [ "FRACTAL" ] },
  894. 602894: { ignoreTextPosts: [ "" ] },
  895. 604604: { missedTextUpdates: [ "605127", "606702" ] },
  896. 609653: { missedTextUpdates: [ "610108", "610137" ] },
  897. 611369: { wrongImageUpdates: [ "620890" ] },
  898. 611997: { missedTextUpdates: [ "612102", "612109" ], wrongTextUpdates: [ "617447" ] },
  899. 613977: { missedTextUpdates: [ "614036" ] },
  900. 615246: { missedTextUpdates: [ "638243", "638245", "638246", "638248" ] },
  901. 615752: { ignoreTextPosts: [ "Uplifted" ] },
  902. 617061: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  903. 617484: { missedTextUpdates: [ "617509", "617830" ] },
  904. 618712: { missedTextUpdates: [ "619097", "619821", "620260" ] },
  905. 620830: { missedAuthors: [ "913f0d" ], ignoreTextPosts: [ "", "Sky-jaws" ] },
  906. 623611: { ignoreTextPosts: [ "!5tTWT1eydY" ] },
  907. 623897: { wrongTextUpdates: [ "625412" ] },
  908. 625364: { missedTextUpdates: [ "635199" ] },
  909. 625814: { missedAuthors: [ "330ce5", "f79974", "53688c", "a19cd5", "defceb" ], missedTextUpdates: [ "625990" ], ignoreTextPosts: [ "" ] },
  910. 627139: { ignoreTextPosts: [ "", "Seal" ] },
  911. 628023: { missedTextUpdates: [ "628323", "629276", "629668" ] },
  912. 628357: { ignoreTextPosts: [ "" ] },
  913. 632345: { ignoreTextPosts: [ "!TEEDashxDA" ] },
  914. 632823: { missedTextUpdates: [ "632860", "633225", "633632", "633649", "633723", "634118" ], ignoreTextPosts: [ "" ] },
  915. 633187: { missedTextUpdates: [ "633407", "633444", "634031", "634192", "634462" ] },
  916. 633487: { missedAuthors: [ "8b8b34", "fe7a48", "20ca72", "668d91" ] },
  917. 634122: { ignoreTextPosts: [ "Apollo" ] },
  918. 639549: { ignoreTextPosts: [ "Apollo" ] },
  919. 641286: { missedTextUpdates: [ "641650" ] },
  920. 642667: { missedTextUpdates: [ "643113" ] },
  921. 642726: { missedTextUpdates: [ "648209", "651723" ] },
  922. 643327: { ignoreTextPosts: [ "" ] },
  923. 644179: { missedTextUpdates: [ "647317" ] },
  924. 645426: { missedTextUpdates: [ "651214", "670665", "671751", "672911", "674718", "684082" ] },
  925. 648109: { missedTextUpdates: [ "711809", "711811" ] },
  926. 648646: { missedTextUpdates: [ "648681" ] },
  927. 651220: { missedTextUpdates: [ "653791" ] },
  928. 651382: { missedAuthors: [ "bbfc3d" ] },
  929. 651540: { missedTextUpdates: [ "651629" ] },
  930. 655158: { ignoreTextPosts: [ "" ] },
  931. 662096: { ignoreTextPosts: [ "" ] },
  932. 662196: { missedAuthors: [ "Penelope" ], ignoreTextPosts: [ "", "Brom", "Wire" ] },
  933. 662452: { ignoreTextPosts: [ "" ] },
  934. 662661: { ignoreTextPosts: [ "" ] },
  935. 663088: { missedAuthors: [ "f68a09", "8177e7" ], ignoreTextPosts: [ "", "!5tTWT1eydY", "Wire", "Brom", "Apollo", "Arhra" ] },
  936. 663996: { missedTextUpdates: [ "673890" ] },
  937. 668009: { missedTextUpdates: [ "668227" ] },
  938. 668216: { imageQuest: false},
  939. 669206: { imageQuest: true, missedAuthors: [ "75347e" ] },
  940. 672060: { missedTextUpdates: [ "673216" ] },
  941. 673444: { ignoreTextPosts: [ "" ] },
  942. 673575: { missedAuthors: [ "a6f913", "3bc92d" ], ignoreTextPosts: [ "!5tTWT1eydY" ] },
  943. 673811: { missedTextUpdates: [ "682275", "687221", "687395", "688995" ], ignoreTextPosts: [ "" ] },
  944. 677271: { missedTextUpdates: [ "677384" ] },
  945. 678114: { imageQuest: false},
  946. 678608: { missedTextUpdates: [ "678789" ] },
  947. 679357: { missedTextUpdates: [ "679359", "679983" ] },
  948. 680125: { ignoreTextPosts: [ "", "BritishHat" ] },
  949. 680206: { missedAuthors: [ "Gnuk" ] },
  950. 681620: { missedAuthors: [ "d9faec" ] },
  951. 683261: { missedAuthors: [ "3/8 MPP, 4/4 MF" ] },
  952. 686590: { imageQuest: false},
  953. 688371: { missedTextUpdates: [ "696249", "696257" ], ignoreTextPosts: [ "", "Chaos", "Ariadne", "Melinoe", "\"F\"ingGenius" ] },
  954. 691136: { missedTextUpdates: [ "697620" ], ignoreTextPosts: [ "" ], wrongImageUpdates: [ "706696" ] },
  955. 691255: { ignoreTextPosts: [ "" ] },
  956. 692093: { missedAuthors: [ "Bergeek" ], ignoreTextPosts: [ "Boxdog" ] },
  957. 692872: { missedTextUpdates: [ "717187" ] },
  958. 693509: { missedAuthors: [ "640f86" ] },
  959. 693648: { missedTextUpdates: [ "694655" ] },
  960. 694230: { ignoreTextPosts: [ "" ] },
  961. 700573: { missedTextUpdates: [ "702352", "720330" ], ignoreTextPosts: [ "" ] },
  962. 701456: { ignoreTextPosts: [ "" ] },
  963. 702865: { ignoreTextPosts: [ "" ] },
  964. 705639: { wrongTextUpdates: [ "794696" ] },
  965. 706303: { missedAuthors: [ "5a8006" ] },
  966. 706439: { missedTextUpdates: [ "714791" ] },
  967. 706938: { ignoreTextPosts: [ "" ] },
  968. 711320: { missedTextUpdates: [ "720646", "724022" ] },
  969. 712179: { missedTextUpdates: [ "712255", "715182" ] },
  970. 712785: { ignoreTextPosts: [ "" ] },
  971. 713042: { missedTextUpdates: [ "713704" ] },
  972. 714130: { imageQuest: true},
  973. 714290: { missedTextUpdates: [ "714307", "714311" ] },
  974. 714858: { ignoreTextPosts: [ "" ] },
  975. 715796: { ignoreTextPosts: [ "" ] },
  976. 717114: { missedTextUpdates: [ "717454", "717628" ] },
  977. 718797: { missedAuthors: [ "FRACTAL on the go" ] },
  978. 718844: { missedAuthors: [ "kome", "Vik", "Friptag" ], missedTextUpdates: [ "721242" ] },
  979. 719505: { ignoreTextPosts: [ "" ] },
  980. 719579: { imageQuest: false},
  981. 722585: { wrongTextUpdates: [ "724938" ] },
  982. 726944: { ignoreTextPosts: [ "" ] },
  983. 727356: { ignoreTextPosts: [ "" ] },
  984. 727581: { missedTextUpdates: [ "728169" ] },
  985. 727677: { ignoreTextPosts: [ "Melinoe" ] },
  986. 728411: { missedTextUpdates: [ "728928" ] },
  987. 730993: { missedTextUpdates: [ "731061" ] },
  988. 732214: { imageQuest: true, wrongTextUpdates: [ "732277" ] },
  989. 734610: { ignoreTextPosts: [ "D3w" ] },
  990. 736484: { ignoreTextPosts: [ "Roman" ], wrongImageUpdates: [ "750212", "750213", "750214" ] },
  991. 741609: { missedTextUpdates: [ "754524" ] },
  992. 743976: { ignoreTextPosts: [ "", "Typo" ] },
  993. 745694: { ignoreTextPosts: [ "Crunchysaurus" ] },
  994. 750281: { ignoreTextPosts: [ "Autozero" ] },
  995. 752572: { missedTextUpdates: [ "752651", "752802", "767190" ] },
  996. 754415: { missedAuthors: [ "Apollo", "riotmode", "!0iuTMXQYY." ], ignoreTextPosts: [ "", "!5tTWT1eydY", "!0iuTMXQYY.", "Indonesian Gentleman" ] },
  997. 755378: { missedAuthors: [ "!Ykw7p6s1S." ] },
  998. 758668: { ignoreTextPosts: [ "LD" ] },
  999. 767346: { ignoreTextPosts: [ "" ] },
  1000. 768858: { ignoreTextPosts: [ "LD" ] },
  1001. 774368: { missedTextUpdates: [ "774500" ] },
  1002. 774930: { missedTextUpdates: [ "794040" ] },
  1003. 778045: { missedTextUpdates: [ "778427", "779363" ] },
  1004. 779564: { ignoreTextPosts: [ "" ] },
  1005. 784068: { wrongTextUpdates: [ "785618" ] },
  1006. 785044: { wrongTextUpdates: [ "801329" ] },
  1007. 789976: { missedTextUpdates: [ "790596", "793934", "800875", "832472" ] },
  1008. 794320: { wrongTextUpdates: [ "795183" ] },
  1009. 798380: { missedTextUpdates: [ "799784", "800444", "800774", "800817", "801212" ] },
  1010. 799546: { missedTextUpdates: [ "801103", "802351", "802753" ] },
  1011. 799612: { missedTextUpdates: [ "799968", "801579" ] },
  1012. 800605: { missedAuthors: [ "Boris Calija", "3373e2", "2016eb", "a80028" ], ignoreTextPosts: [ "", "Boris Calija" ] },
  1013. 802411: { missedTextUpdates: [ "805002" ] },
  1014. 807972: { wrongTextUpdates: [ "811969" ] },
  1015. 809039: { wrongImageUpdates: [ "817508", "817511" ] },
  1016. 811957: { ignoreTextPosts: [ "via Discord" ] },
  1017. 814448: { missedTextUpdates: [ "817938" ] },
  1018. 817541: { missedAuthors: [ "Raptie" ] },
  1019. 822552: { imageQuest: false},
  1020. 823831: { missedAuthors: [ "Retro-LOPIS" ] },
  1021. 827264: { ignoreTextPosts: [ "LD", "DogFace" ] },
  1022. 830006: { missedAuthors: [ "Amaranth" ] },
  1023. 835062: { ignoreTextPosts: [ "Curves" ] },
  1024. 835750: { missedTextUpdates: [ "836870" ] },
  1025. 836521: { wrongTextUpdates: [ "848748" ] },
  1026. 837514: { ignoreTextPosts: [ "LD" ] },
  1027. 839906: { missedTextUpdates: [ "845724" ] },
  1028. 840029: { missedTextUpdates: [ "840044", "840543" ] },
  1029. 841851: { ignoreTextPosts: [ "Serpens", "Joy" ] },
  1030. 842392: { missedTextUpdates: [ "842434", "842504", "842544" ] },
  1031. 844537: { missedTextUpdates: [ "847326" ] },
  1032. 848887: { imageQuest: true, wrongTextUpdates: [ "851878" ] },
  1033. 854088: { missedTextUpdates: [ "860219" ], ignoreTextPosts: [ "Ursula" ] },
  1034. 854203: { ignoreTextPosts: [ "Zenthis" ] },
  1035. 857294: { wrongTextUpdates: [ "857818" ] },
  1036. 858913: { imageQuest: false},
  1037. 863241: { missedTextUpdates: [ "863519" ] },
  1038. 865754: { missedTextUpdates: [ "875371" ], ignoreTextPosts: [ "???" ] },
  1039. 869242: { ignoreTextPosts: [ "" ] },
  1040. 871667: { missedTextUpdates: [ "884575" ] },
  1041. 876808: { imageQuest: false},
  1042. 879456: { missedTextUpdates: [ "881847" ] },
  1043. 881097: { missedTextUpdates: [ "881292", "882339" ] },
  1044. 881374: { ignoreTextPosts: [ "LD" ] },
  1045. 885481: { imageQuest: false, wrongTextUpdates: [ "886892" ] },
  1046. 890023: { missedAuthors: [ "595acb" ] },
  1047. 897318: { missedTextUpdates: [ "897321", "897624" ] },
  1048. 897846: { missedTextUpdates: [ "897854", "897866" ] },
  1049. 898917: { missedAuthors: [ "Cee (mobile)" ] },
  1050. 900852: { missedTextUpdates: [ "900864" ] },
  1051. 904316: { missedTextUpdates: [ "904356", "904491" ] },
  1052. 907309: { missedTextUpdates: [ "907310" ] },
  1053. 913803: { ignoreTextPosts: [ "Typo" ] },
  1054. 915945: { missedTextUpdates: [ "916021" ] },
  1055. 917513: { missedTextUpdates: [ "917515" ] },
  1056. 918806: { missedTextUpdates: [ "935207" ] },
  1057. 921083: { ignoreTextPosts: [ "LawyerDog" ] },
  1058. 923174: { ignoreTextPosts: [ "Marn", "MarnMobile" ] },
  1059. 924317: { ignoreTextPosts: [ "" ] },
  1060. 926927: { missedTextUpdates: [ "928194" ] },
  1061. 929545: { missedTextUpdates: [ "929634" ] },
  1062. 930854: { missedTextUpdates: [ "932282" ] },
  1063. 934026: { missedTextUpdates: [ "934078", "934817" ] },
  1064. 935464: { missedTextUpdates: [ "935544", "935550", "935552", "935880" ] },
  1065. 939572: { missedTextUpdates: [ "940402" ] },
  1066. 940835: { missedTextUpdates: [ "941005", "941067", "941137", "941226" ] },
  1067. 1000012: { missedAuthors: [ "Happiness" ] },
  1068. };
  1069. }
  1070. return this.allFixes;
  1071. }
  1072. }
  1073.  
  1074. //More or less standard XMLHttpRequest wrapper
  1075. //Input: url
  1076. //Output: Promise that resolves into the XHR object (or a HTTP error code)
  1077. class Xhr {
  1078. static get(url) {
  1079. return new Promise(function(resolve, reject) {
  1080. const xhr = new XMLHttpRequest();
  1081. xhr.onreadystatechange = function(e) {
  1082. if (xhr.readyState === 4) {
  1083. if (xhr.status === 200) {
  1084. resolve(xhr);
  1085. }
  1086. else {
  1087. reject(xhr.status);
  1088. }
  1089. }
  1090. };
  1091. xhr.ontimeout = function () {
  1092. reject("timeout");
  1093. };
  1094. xhr.open("get", url, true);
  1095. xhr.send();
  1096. });
  1097. }
  1098. }
  1099.  
  1100. //QuestReader class
  1101. //Input: none
  1102. //Output: none
  1103. //Usage: new QuestReader.init(settings);
  1104. //settings: a settings object obtained from the object's onSettingsChanged event, allowing you to store settings
  1105. class QuestReader {
  1106. constructor() {
  1107. this.updates = [];
  1108. this.sequences = [];
  1109. this.onSettingsChanged = null;
  1110. this.setSettings(this.getDefaultSettings());
  1111. this.elementsCache = new Map();
  1112. }
  1113.  
  1114. init(settings) {
  1115. var updateAnalyzer = new UpdateAnalyzer();
  1116. var results = updateAnalyzer.analyzeQuest(document); //run UpdateAnalyzer to determine which posts are updates and what not
  1117. this.threadID = updateAnalyzer.threadID;
  1118. this.updates = this.getUpdatePostGroups(results.postTypes, results.postUsers); //organize posts into groups where each group has one update post and the following suggestions
  1119. this.sequences = this.getUpdateSequences(); //a list of unique update sequences
  1120. this.cacheElements(); //cache post elements for faster access
  1121. this.insertControls(); //insert html elements for controls
  1122. this.insertStyling(); //insert html elements for styling
  1123. this.insertEvents(); //insert our own button events plus some global events
  1124. this.modifyLayout(); //change the default layout by moving elements around to make them fit better
  1125. this.setSettings(this.validateSettings(settings)); //load settings
  1126. this.refresh(true); //hide all posts and show only the relevant ones; enable/disable/update controls
  1127. this.getLinksFromWiki(); //get quest's wiki link, disthread link, and other thread links
  1128. }
  1129.  
  1130. cacheElements() {
  1131. document.querySelectorAll(".postwidth > a[name]").forEach(anchor => {
  1132. if (anchor.name == "s") {
  1133. return;
  1134. }
  1135. var postID = anchor.name;
  1136. var parent = anchor.parentElement;
  1137. while (parent && parent.nodeName != "TABLE" && parent.nodeName != "FORM" && parent.classList != "de-oppost") {
  1138. parent = parent.parentElement;
  1139. }
  1140. if (parent) {
  1141. var key = parseInt(postID);
  1142. if (!this.elementsCache.has(key)) {
  1143. this.elementsCache.set(key, parent);
  1144. }
  1145. }
  1146. });
  1147. }
  1148.  
  1149. getUpdatePostGroups(postTypes, postUsers) {
  1150. var updatePostGroups = [];
  1151. var currentPostGroup = { updatePostID: 0, suggestions: [], authorComments: [] };
  1152. var postTypesArray = [...postTypes];
  1153. this.total = { authorComments: 0, suggestions: 0, suggesters: 0};
  1154. //create post groups
  1155. for (let i = postTypesArray.length - 1; i >= 0; i--) {
  1156. if (postTypesArray[i][1] == PostType.UPDATE) {
  1157. currentPostGroup.updatePostID = postTypesArray[i][0];
  1158. updatePostGroups.unshift(currentPostGroup);
  1159. currentPostGroup = { updatePostID: 0, suggestions: [], authorComments: [] };
  1160. }
  1161. else if (postTypesArray[i][1] == PostType.AUTHORCOMMENT) {
  1162. currentPostGroup.authorComments.unshift(postTypesArray[i][0]);
  1163. this.total.authorComments++;
  1164. }
  1165. else { //PostType.SUGGESTION
  1166. currentPostGroup.suggestions.unshift(postTypesArray[i][0]);
  1167. this.total.suggestions++;
  1168. }
  1169. }
  1170. //create sequence groups
  1171. var currentUpdateSequence = [];
  1172. updatePostGroups.forEach(postGroup => {
  1173. currentUpdateSequence.push(postGroup);
  1174. postGroup.sequence = currentUpdateSequence;
  1175. if (postGroup.suggestions.length > 0) {
  1176. currentUpdateSequence = [];
  1177. }
  1178. });
  1179. //set post suggesters
  1180. var allSuggesters = new Set();
  1181. updatePostGroups.forEach(postGroup => {
  1182. postGroup.suggesters = postGroup.suggestions.reduceRight((suggesters, el) => {
  1183. var suggester = postUsers.get(el);
  1184. suggesters.add(suggester);
  1185. allSuggesters.add(suggester);
  1186. return suggesters;
  1187. }, new Set());
  1188. postGroup.suggesters = [...postGroup.suggesters];
  1189. });
  1190.  
  1191. this.total.suggesters = allSuggesters.size;
  1192. return updatePostGroups;
  1193. }
  1194.  
  1195. getUpdateSequences() {
  1196. var sequences = [];
  1197. this.updates.forEach(update => {
  1198. if (update.sequence !== sequences[sequences.length - 1]) {
  1199. sequences.push(update.sequence);
  1200. }
  1201. });
  1202. return sequences;
  1203. }
  1204.  
  1205. currentUpdate() {
  1206. return this.updates[this.currentUpdateIndex];
  1207. }
  1208.  
  1209. firstUpdate() {
  1210. return this.updates[0];
  1211. }
  1212.  
  1213. lastUpdate() {
  1214. return this.updates[this.updates.length - 1];
  1215. }
  1216.  
  1217. refresh(checkHash = false, scroll = true) {
  1218. //checkHash: if true and the url has a hash, show the update that contains the post ID in the hash, and scroll to this update
  1219. //scroll: if true, will scroll to the current update
  1220. var scrollToPostID = null;
  1221. if (checkHash && document.defaultView.location.hash) {
  1222. scrollToPostID = parseInt(document.defaultView.location.hash.replace("#", ""));
  1223. this.currentUpdateIndex = this.findUpdate(scrollToPostID);
  1224. }
  1225. else if (this.viewMode == "all" && this.currentUpdate() != this.firstUpdate()) {
  1226. scrollToPostID = this.currentUpdate().updatePostID;
  1227. }
  1228. this.hideAll();
  1229. this.showCurrentUpdates();
  1230. if (checkHash && scrollToPostID) {
  1231. this.showPost(scrollToPostID); //in case we want to scroll to a hidden suggestion, we want to show it first
  1232. }
  1233. this.updateControls();
  1234. if (scroll) {
  1235. var scrollToElement = scrollToPostID ? this.elementsCache.get(scrollToPostID) : document.querySelector(".qrControlsTop");
  1236. var scrollOptions = { behavior: "smooth", block: "start", };
  1237. setTimeout(() => { scrollToElement.scrollIntoView(scrollOptions); }, 0);
  1238. }
  1239. }
  1240.  
  1241. hideAll() {
  1242. for (var key of this.elementsCache.keys()) {
  1243. var el = this.elementsCache.get(key);
  1244. if (key == this.threadID) {
  1245. [...el.children].forEach(opPostChildEl => {
  1246. if (opPostChildEl.className === "postwidth" || opPostChildEl.nodeName === "BLOCKQUOTE" || opPostChildEl.nodeName === "A" || opPostChildEl.className === "de-refmap") {
  1247. opPostChildEl.classList.add("hidden");
  1248. }
  1249. });
  1250. }
  1251. else {
  1252. el.classList.add("hidden");
  1253. }
  1254. }
  1255. }
  1256.  
  1257. findUpdate(postID) {
  1258. for (var i = 0; i < this.updates.length; i++) {
  1259. if (this.updates[i].updatePostID == postID || this.updates[i].suggestions.indexOf(postID) != -1 || this.updates[i].authorComments.indexOf(postID) != -1) {
  1260. return i;
  1261. }
  1262. }
  1263. }
  1264.  
  1265. showCurrentUpdates() {
  1266. var updatesToShow = {
  1267. single: [ this.currentUpdate() ],
  1268. sequence: this.currentUpdate().sequence,
  1269. all: this.updates,
  1270. };
  1271. var updatesToExpand = {};
  1272. var currentSequenceIndex = this.sequences.indexOf(this.currentUpdate().sequence);
  1273. updatesToExpand.single = [this.updates[this.currentUpdateIndex - 1], this.currentUpdate(), this.updates[this.currentUpdateIndex + 1]].filter(el => !!el);
  1274. updatesToExpand.sequence = [...this.sequences[currentSequenceIndex - 1] || [], ...this.sequences[currentSequenceIndex], ...this.sequences[currentSequenceIndex + 1] || []];
  1275. //expanding images on the fly when in full thread view is a bit janky when navigating up
  1276. updatesToExpand.all = [this.currentUpdate(), this.updates[this.currentUpdateIndex + 1]].filter(el => !!el);
  1277. //updatesToExpand.all = this.updates;
  1278.  
  1279. updatesToShow[this.viewMode].forEach(update => this.showUpdate(update));
  1280. updatesToExpand[this.viewMode].forEach(update => this.expandUpdateImages(update));
  1281. }
  1282.  
  1283. expandUpdateImages(update) {
  1284. var postsToExpand = [ update.updatePostID ];
  1285. if (this.expandImages != "updates") {
  1286. postsToExpand = postsToExpand.concat(update.suggestions, update.authorComments);
  1287. }
  1288. postsToExpand.forEach(postID => {
  1289. var el = this.elementsCache.get(postID);
  1290. var link = el.querySelector(".postwidth > .filesize > a");
  1291. //var fileTypes = [ ".png", ".jpg", ".jpeg", ".gif" ];
  1292. //if (!link || fileTypes.indexOf((link.href.match(this.regex.fileExtension) || [])[0].toLowerCase()) < 0) {
  1293. if (!link || !link.onclick) { //if there is no link, or the link doesn't have an onclick event set, then we know it's not an expandable image
  1294. return;
  1295. }
  1296. var img = el.querySelector(`#thumb${postID} > img`);
  1297. if (img.previousElementSibling && img.previousElementSibling.nodeName === "CANVAS") {
  1298. img.previousElementSibling.remove(); //remove canvas covering the image
  1299. img.style.removeProperty("display");
  1300. }
  1301.  
  1302. var expanded = img.src === link.href;
  1303. if (!expanded && (this.expandImages == "all" || (this.expandImages == "updates" && postID == update.updatePostID))) {
  1304. img.setAttribute("thumbsrc", img.src);
  1305. img.removeAttribute("onmouseover");
  1306. img.removeAttribute("onmouseout");
  1307. img.src = link.href;
  1308. }
  1309. // contract images as well
  1310. else if (expanded && (this.expandImages == "none" || (this.expandImages == "updates" && postID != update.updatePostID))) {
  1311. if (img.hasAttribute("thumbsrc") && !img.getAttribute("thumbsrc").endsWith("spoiler.png")) {
  1312. img.src = img.getAttribute("thumbsrc");
  1313. }
  1314. else {
  1315. link.click();
  1316. }
  1317. }
  1318. });
  1319. }
  1320.  
  1321. showUpdate(update) {
  1322. this.showPost(update.updatePostID);
  1323. if (this.showSuggestions == "all" || this.showSuggestions == "last" && update == this.lastUpdate()) {
  1324. update.suggestions.forEach(postID => this.showPost(postID));
  1325. }
  1326. if (this.showAuthorComments == "all" || this.showAuthorComments == "last" && update == this.lastUpdate()) {
  1327. update.authorComments.forEach(postID => this.showPost(postID));
  1328. }
  1329. }
  1330.  
  1331. showPost(postID) {
  1332. var el = this.elementsCache.get(postID);
  1333. if (postID == this.threadID) {
  1334. [...el.children].forEach(childEl => {
  1335. if (childEl.classList.contains("postwidth") || childEl.nodeName === "BLOCKQUOTE") {
  1336. childEl.classList.remove("hidden");
  1337. }
  1338. });
  1339. }
  1340. else {
  1341. el.classList.remove("hidden");
  1342. }
  1343. }
  1344.  
  1345. showFirst() {
  1346. var newUpdateIndex = 0;
  1347. this.changeIndex(newUpdateIndex);
  1348. }
  1349.  
  1350. showLast() {
  1351. var newUpdateIndex = this.viewMode == "sequence" ? this.updates.indexOf(this.sequences[this.sequences.length - 1][0]) : this.updates.length - 1;
  1352. this.changeIndex(newUpdateIndex);
  1353. }
  1354.  
  1355. showNext() {
  1356. var newUpdateIndex = this.currentUpdateIndex + 1;
  1357. if (this.viewMode == "sequence") { //move to the first update in the next sequence
  1358. var currentSequenceIndex = this.sequences.indexOf(this.currentUpdate().sequence);
  1359. newUpdateIndex = currentSequenceIndex < this.sequences.length - 1 ? this.updates.indexOf(this.sequences[currentSequenceIndex + 1][0]) : this.updates.length;
  1360. }
  1361. this.changeIndex(newUpdateIndex);
  1362. }
  1363.  
  1364. showPrevious() {
  1365. var newUpdateIndex = this.currentUpdateIndex - 1;
  1366. if (this.viewMode == "sequence") {
  1367. var currentSequenceIndex = this.sequences.indexOf(this.currentUpdate().sequence);
  1368. newUpdateIndex = currentSequenceIndex > 0 ? this.updates.indexOf(this.sequences[currentSequenceIndex - 1][0]) : -1;
  1369. }
  1370. this.changeIndex(newUpdateIndex);
  1371. }
  1372.  
  1373. changeIndex(newUpdateIndex) {
  1374. if (newUpdateIndex === this.currentUpdateIndex || newUpdateIndex < 0 || newUpdateIndex > this.updates.length - 1) {
  1375. return;
  1376. }
  1377. this.currentUpdateIndex = newUpdateIndex;
  1378. this.refresh(false, true);
  1379. this.settingsChanged();
  1380. }
  1381.  
  1382. getDefaultSettings() {
  1383. return {
  1384. currentUpdateIndex: 0,
  1385. viewMode: "all", //all, single, sequence
  1386. showSuggestions: "all", //none, last, all
  1387. showAuthorComments: "all", //none, last, all
  1388. replyFormLocation: "top", //top, bottom
  1389. expandImages: "none", //none, updates, all
  1390. showUpdateInfo: false, //false, true
  1391. showReplyForm: true, //false, true
  1392. };
  1393. }
  1394.  
  1395. setSettings(settings) {
  1396. if (settings) {
  1397. for(var settingName in settings) {
  1398. this[settingName] = settings[settingName];
  1399. }
  1400. }
  1401. }
  1402.  
  1403. validateSettings(settings) {
  1404. if (!settings) {
  1405. return settings;
  1406. }
  1407. if (settings.currentUpdateIndex < 0) settings.currentUpdateIndex = 0;
  1408. if (settings.currentUpdateIndex >= this.updates.length) settings.currentUpdateIndex = this.updates.length - 1;
  1409. for (var prop in settings) {
  1410. if (typeof(settings[prop]) !== typeof(this[prop])) {
  1411. settings[prop] = this[prop];
  1412. }
  1413. }
  1414. return settings;
  1415. }
  1416.  
  1417. settingsChanged() {
  1418. if (this.onSettingsChanged) {
  1419. var settings = {
  1420. currentUpdateIndex: this.currentUpdateIndex,
  1421. viewMode: this.viewMode,
  1422. showSuggestions: this.showSuggestions,
  1423. showAuthorComments: this.showAuthorComments,
  1424. replyFormLocation: this.replyFormLocation,
  1425. expandImages: this.expandImages,
  1426. showUpdateInfo: this.showUpdateInfo,
  1427. showReplyForm: this.showReplyForm,
  1428. };
  1429. this.onSettingsChanged(settings);
  1430. }
  1431. }
  1432.  
  1433. toggleSettingsControls(e) {
  1434. e.preventDefault(); //prevent scrolling to the top when clicking the link
  1435. var settingsEl = document.querySelector(".qrSettingsControls");
  1436. settingsEl.classList.toggle("collapsedHeight");
  1437. var label = e.target;
  1438. label.text = settingsEl.classList.contains("collapsedHeight") ? "Settings" : "Hide Settings";
  1439. }
  1440.  
  1441. toggleReplyForm(e) {
  1442. e.preventDefault(); //prevent scrolling to the top when clicking the link
  1443. this.showReplyForm = !document.forms.postform.classList.toggle("hidden");
  1444. document.querySelector("#qrReplyFormRestore").classList.toggle("hidden", this.showReplyForm);
  1445. this.settingsChanged();
  1446. }
  1447.  
  1448. popoutReplyForm(e) {
  1449. e.preventDefault(); //prevent scrolling to the top when clicking the link
  1450. var replyForm = document.forms.postform;
  1451. var floating = replyForm.classList.toggle("qrPopout");
  1452. if (floating) {
  1453. if (!this.replyFormDraggable) {
  1454. var rect = document.querySelector(".postform").getBoundingClientRect();
  1455. replyForm.style.left = `${document.documentElement.clientWidth - rect.width - 10}px`;
  1456. replyForm.style.top = `${(document.defaultView.innerHeight - rect.height) * 0.75}px`;
  1457. }
  1458. this.replyFormDraggable = new document.defaultView.Draggable("postform", { handle: "qrReplyFormHeader" });
  1459. }
  1460. else {
  1461. this.replyFormDraggable.destroy();
  1462. }
  1463. }
  1464.  
  1465. changeThread(e) {
  1466. document.defaultView.location.href = e.target.value;
  1467. }
  1468.  
  1469. updateSettings() {
  1470. this.viewMode = document.getElementById("qrShowUpdatesDropdown").value;
  1471. this.showSuggestions = document.getElementById("qrShowSuggestionsDropdown").value;
  1472. this.showAuthorComments = document.getElementById("qrShowAuthorCommentsDropdown").value;
  1473. this.replyFormLocation = document.getElementById("qrReplyFormLocationDropdown").value;
  1474. this.expandImages = document.getElementById("qrExpandImagesDropdown").value;
  1475. this.showUpdateInfo = document.getElementById("qrShowUpdateInfoCheckbox").checked === true;
  1476. this.refresh(false, false);
  1477. this.settingsChanged();
  1478. }
  1479.  
  1480. getLinksFromWiki() {
  1481. Xhr.get(`/w/index.php?search=${this.threadID}&fulltext=1&limit=500`).then(xhr => {
  1482. var threadID = xhr.responseURL.match(new RegExp("search=([0-9]+)"))[1];
  1483. let doc = document.implementation.createHTMLDocument(); //we create a HTML document, but don't load the images or scripts therein
  1484. doc.documentElement.innerHTML = xhr.response;
  1485. var results = [...doc.querySelectorAll(".searchmatch")].filter(el => el.textContent == threadID);
  1486. if (results.length === 0) {
  1487. return;
  1488. }
  1489. //filter wiki search results to the one that has the threadID in the quest info box
  1490. var theRightOne = results.filter(el => {var p = el.previousSibling; return p && p.nodeType == Node.TEXT_NODE && p.textContent.match(new RegExp("[0-9]=$")); });
  1491. if (theRightOne.length === 0) {
  1492. return;
  1493. }
  1494. var wikiUrl = theRightOne[0].parentElement.previousElementSibling.querySelector("a").href;
  1495. document.querySelectorAll(".qrWikiLink").forEach(link => { link.href = wikiUrl; link.style.removeProperty("color"); });
  1496. Xhr.get(wikiUrl).then(xhr => {
  1497. //parse quest wiki
  1498. let doc = document.implementation.createHTMLDocument();
  1499. doc.documentElement.innerHTML = xhr.response;
  1500. var links = [...doc.querySelectorAll(".infobox a")];
  1501. //get latest disthread link
  1502. var disThreadLinks = links.filter(l => l.href.indexOf("/questdis/") >= 0);
  1503. if (disThreadLinks.length > 0) {
  1504. var disThreadUrl = disThreadLinks[disThreadLinks.length - 1].href;
  1505. document.querySelectorAll(".qrDisLink").forEach(link => { link.href = disThreadUrl; link.style.removeProperty("color"); });
  1506. }
  1507. //get quest threads
  1508. var threadLinks = links.filter(l => l.href.indexOf("/quest/") >= 0 || l.href.indexOf("/questarch/") >= 0 || l.href.indexOf("/graveyard/") >= 0).filter(l => l.href.indexOf("image-for") < 0);
  1509. var currentThreadLink = threadLinks.find(link => link.href.indexOf(this.threadID) >= 0);
  1510. if (currentThreadLink) {
  1511. //only links within the same box
  1512. threadLinks = [...currentThreadLink.parentElement.parentElement.querySelectorAll("a")];
  1513. threadLinks = threadLinks.filter(l => l.href.indexOf("/quest/") >= 0 || l.href.indexOf("/questarch/") >= 0 || l.href.indexOf("/graveyard/") >= 0).filter(l => l.href.indexOf("image-for") < 0);
  1514. var threadOptionsHtml = threadLinks.reverse().reduceRight((acc, link) => {
  1515. var threadName = (link.textContent == "Thread" && threadLinks.length === 1) ? "Thread 1" : link.textContent;
  1516. acc += `<option value="${link.href}">${threadName}</option>`;
  1517. return acc;
  1518. }, "");
  1519. document.querySelectorAll("#qrThreadLinksDropdown").forEach(dropdown => {
  1520. dropdown.innerHTML = threadOptionsHtml;
  1521. dropdown.value = currentThreadLink.href;
  1522. });
  1523. }
  1524. //get quest author and title
  1525. var infoboxHeader = doc.querySelector(".infobox big");
  1526. if (infoboxHeader) {
  1527. var children = [...infoboxHeader.childNodes];
  1528. var questTitle = children.shift().textContent;
  1529. var byAuthors = children.reverse().reduceRight((acc, el) => {acc += el.textContent; return acc;}, "");
  1530. document.title = `${this.hasTitle ? document.title : questTitle}${byAuthors}`;
  1531. document.querySelector(".logo").textContent = document.title;
  1532. }
  1533. });
  1534. });
  1535. }
  1536.  
  1537. updateControls() {
  1538. var leftDisabled = true;
  1539. var rightDisabled = true;
  1540. var current = 1;
  1541. var last = 1;
  1542. var authorCommentsCount;
  1543. var suggestionsCount;
  1544. var suggestersCount;
  1545. if (this.viewMode == "sequence") {
  1546. leftDisabled = this.currentUpdate().sequence == this.firstUpdate().sequence;
  1547. rightDisabled = this.currentUpdate().sequence == this.lastUpdate().sequence;
  1548. current = this.sequences.indexOf(this.currentUpdate().sequence) + 1;
  1549. last = this.sequences.length;
  1550. authorCommentsCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].authorComments.length;
  1551. suggestionsCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].suggestions.length;
  1552. suggestersCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].suggesters.length;
  1553. }
  1554. else {
  1555. leftDisabled = this.currentUpdate() == this.firstUpdate();
  1556. rightDisabled = this.currentUpdate() == this.lastUpdate();
  1557. current = this.currentUpdateIndex + 1;
  1558. last = this.updates.length;
  1559. authorCommentsCount = this.viewMode == "all" ? this.total.authorComments : this.currentUpdate().authorComments.length;
  1560. suggestionsCount = this.viewMode == "all" ? this.total.suggestions : this.currentUpdate().suggestions.length;
  1561. suggestersCount = this.viewMode == "all" ? this.total.suggesters : this.currentUpdate().suggesters.length;
  1562. }
  1563. // buttons
  1564. document.querySelectorAll("#qrShowFirstButton, #qrShowPrevButton").forEach(button => { button.disabled = leftDisabled; });
  1565. document.querySelectorAll("#qrShowNextButton, #qrShowLastButton").forEach(button => { button.disabled = rightDisabled; });
  1566. // update info
  1567. document.querySelectorAll(".qrCurrentPos").forEach(label => { label.textContent = current; label.classList.toggle("crimson", current == last); });
  1568. document.querySelectorAll(".qrTotalPos").forEach(label => { label.textContent = last; });
  1569. document.querySelectorAll(".qrUpdateInfo").forEach(infoContainer => { infoContainer.classList.toggle("hidden", !this.showUpdateInfo); });
  1570. document.querySelectorAll("#qrAuthorCommentsCount").forEach(label => { label.textContent = ` A:${authorCommentsCount}`; });
  1571. document.querySelectorAll("#qrSuggestionsCount").forEach(label => { label.textContent = `S:${suggestionsCount}`; });
  1572. document.querySelectorAll("#qrSuggestersCount").forEach(label => { label.textContent = `U:${suggestersCount}`; });
  1573. // settings
  1574. document.getElementById("qrShowUpdatesDropdown").value = this.viewMode;
  1575. document.getElementById("qrShowSuggestionsDropdown").value = this.showSuggestions;
  1576. document.getElementById("qrShowAuthorCommentsDropdown").value = this.showAuthorComments;
  1577. document.getElementById("qrReplyFormLocationDropdown").value = this.replyFormLocation;
  1578. document.getElementById("qrExpandImagesDropdown").value = this.expandImages;
  1579. document.getElementById("qrShowUpdateInfoCheckbox").checked = this.showUpdateInfo;
  1580. // sticky controls when viewing whole thread
  1581. document.querySelectorAll(".qrNavControls")[1].classList.toggle("stickyBottom", this.viewMode == "all");
  1582. /* // sentinels for full thread view
  1583. var topOfCurrent = 0;
  1584. var bottomOfCurrent = 0;
  1585. if (this.viewMode == "all") {
  1586. if (this.currentUpdate() != this.firstUpdate()) {
  1587. topOfCurrent = this.elementsCache.get(this.currentUpdate().updatePostID).offsetTop;
  1588. }
  1589. if (this.currentUpdate() != this.lastUpdate()) {
  1590. bottomOfCurrent = this.elementsCache.get(this.updates[this.currentUpdateIndex + 1].updatePostID).offsetTop;
  1591. }
  1592. this.sentinelPreviousEl.style.height = `${topOfCurrent}px`; //end of previous is top of current;
  1593. this.sentinelCurrentEl.style.height = `${bottomOfCurrent}px`; //end of current is the top of next
  1594. }
  1595. this.sentinelPreviousEl.classList.toggle("hidden", this.viewMode != "all" || topOfCurrent === 0);
  1596. this.sentinelCurrentEl.classList.toggle("hidden", this.viewMode != "all" || bottomOfCurrent === 0);
  1597. */
  1598. // reply form juggling
  1599. var postarea = document.querySelector(".postarea");
  1600. var replymode = document.querySelector(".replymode");
  1601. var isReplyFormAtTop = (replymode == postarea.previousElementSibling);
  1602. if (this.replyFormLocation == "bottom" && isReplyFormAtTop) { //move it down
  1603. postarea.remove();
  1604. document.body.insertBefore(postarea, document.querySelectorAll(".navbar")[1]);
  1605. document.querySelector(".qrControlsTop").previousElementSibling.insertAdjacentHTML("beforeBegin", "<hr>");
  1606. }
  1607. else if (this.replyFormLocation == "top" && !isReplyFormAtTop) { //move it up
  1608. postarea.remove();
  1609. replymode.insertAdjacentElement("afterEnd", postarea);
  1610. document.querySelector(".qrControlsTop").previousElementSibling.previousElementSibling.remove(); //remove <hr>
  1611. }
  1612. document.forms.postform.classList.toggle("hidden", !this.showReplyForm);
  1613. document.querySelector("#qrReplyFormRestore").classList.toggle("hidden", this.showReplyForm);
  1614. if (this.viewMode == "all" && !this.scrollIntervalHandle) {
  1615. this.scrollIntervalHandle = setInterval(() => { if (this.viewMode == "all" && Date.now() - this.lastScrollTime < 250) { this.handleScroll(); } }, 50);
  1616. }
  1617. else if (this.viewMode != "all" && this.scrollIntervalHandle) {
  1618. clearInterval(this.scrollIntervalHandle);
  1619. this.scrollIntervalHandle = null;
  1620. }
  1621. }
  1622.  
  1623. insertControls() {
  1624. //top controls
  1625. document.querySelector("body > form").insertAdjacentHTML("beforebegin", this.getTopControlsHtml());
  1626. //bottom nav controls
  1627. var del = document.querySelector(".userdelete");
  1628. if (del.parentElement.nodeName == "DIV") {
  1629. del = del.parentElement;
  1630. }
  1631. del.insertAdjacentHTML("beforebegin", this.getBottomControlsHtml());
  1632. //make reply form collapsable
  1633. document.querySelector(".postarea").insertAdjacentHTML("afterBegin", `<div id="qrReplyFormRestore" class="hidden">[<a href="#">Reply</a>]</div>`);
  1634. //reply form header
  1635. document.querySelector(".postform").insertAdjacentHTML("afterBegin", this.getReplyFormHeaderHtml());
  1636.  
  1637. /* //when viewing full thread, we want to detect and remember where we are; something something IntersectionObserver
  1638. document.body.insertAdjacentHTML("afterBegin", `<div class="sentinel hidden"></div><div class="sentinel hidden"></div>`);
  1639. this.sentinelPreviousEl = document.body.firstChild;
  1640. this.sentinelCurrentEl = document.body.firstChild.nextSibling;
  1641. this.sentinelPrevious = new IntersectionObserver((entries, observer) => { this.handleSentinel(entries, observer); }, { rootMargin: "2px" } ); //need to pass the callback like this to keep the context
  1642. this.sentinelCurrent = new IntersectionObserver((entries, observer) => { this.handleSentinel(entries, observer); }, { rootMargin: "2px" } );
  1643. this.sentinelPrevious.observe(this.sentinelPreviousEl);
  1644. this.sentinelCurrent.observe(this.sentinelCurrentEl);
  1645. */
  1646. }
  1647. /*
  1648. handleSentinel(entries, observer) {
  1649. console.log(entries[0]);
  1650. var newUpdateIndex = this.currentUpdateIndex;
  1651. if (observer == this.sentinelPrevious && entries[0].isIntersecting) {
  1652. newUpdateIndex--;
  1653. }
  1654. else if (observer == this.sentinelCurrent && !entries[0].isIntersecting) {
  1655. newUpdateIndex++;
  1656. }
  1657. if (newUpdateIndex != this.currentUpdateIndex && newUpdateIndex >= 0 && newUpdateIndex < this.updates.length) {
  1658. this.currentUpdateIndex = newUpdateIndex;
  1659. this.updateControls();
  1660. this.settingsChanged();
  1661. }
  1662. }
  1663. */
  1664. insertStyling() {
  1665. document.body.insertAdjacentHTML("beforeend", this.getStylingHtml());
  1666. }
  1667.  
  1668. insertEvents() {
  1669. var events = [ //events for our controls
  1670. ["#qrSettingsToggle > a", "click", this.toggleSettingsControls],
  1671. ["#qrShowUpdatesDropdown", "change", this.updateSettings],
  1672. ["#qrShowSuggestionsDropdown", "change", this.updateSettings],
  1673. ["#qrShowAuthorCommentsDropdown", "change", this.updateSettings],
  1674. ["#qrReplyFormLocationDropdown", "change", this.updateSettings],
  1675. ["#qrExpandImagesDropdown", "change", this.updateSettings],
  1676. ["#qrThreadLinksDropdown", "change", this.changeThread],
  1677. ["#qrShowUpdateInfoCheckbox", "click", this.updateSettings],
  1678. ["#qrShowFirstButton", "click", this.showFirst],
  1679. ["#qrShowPrevButton", "click", this.showPrevious],
  1680. ["#qrShowNextButton", "click", this.showNext],
  1681. ["#qrShowLastButton", "click", this.showLast],
  1682. ["#qrReplyFormRestore > a", "click", this.toggleReplyForm],
  1683. ["#qrReplyFormPopout", "click", this.popoutReplyForm],
  1684. ["#qrReplyFormMinimize", "click", this.toggleReplyForm],
  1685. ];
  1686. events.forEach(params => {
  1687. document.querySelectorAll(params[0]).forEach(el => {
  1688. el.addEventListener(params[1], (e) => { params[2].call(this, e); }); //need to pass "this" as context, otherwise it gets set to the caller
  1689. });
  1690. });
  1691.  
  1692. // global events
  1693. document.defaultView.addEventListener("hashchange", (e) => { //if the #hash at the end of url changes, it means the user clicked a post link and we need to show him the update that contains that post
  1694. if (document.defaultView.location.hash) {
  1695. this.refresh(true);
  1696. }
  1697. });
  1698.  
  1699. this.lastScrollTime = 0;
  1700. document.defaultView.addEventListener("wheel", (e) => { //after the wheeling has finished, check if the user
  1701. this.lastScrollTime = Date.now();
  1702. });
  1703.  
  1704. document.addEventListener("keydown", (e) => {
  1705. var inputTypes = ["text", "password", "number", "email", "tel", "url", "search", "date", "datetime", "datetime-local", "time", "month", "week"];
  1706. if (e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || (e.target.tagName === "INPUT" && inputTypes.indexOf(e.target.type) >= 0)) {
  1707. return; //prevent our keyboard shortcuts when focused on a text input field
  1708. }
  1709. if (e.altKey) { //alt+left arrow, or alt+right arrow, for the obvious reasons we don't want to handle those
  1710. return;
  1711. }
  1712. if (e.key == "ArrowRight") {
  1713. e.preventDefault();
  1714. this.showNext();
  1715. }
  1716. else if (e.key == "ArrowLeft") {
  1717. e.preventDefault();
  1718. this.showPrevious();
  1719. }
  1720. // I'm not sure if binding Home and End would be a desirable behavior considering how rarely the two buttons are normally used
  1721. /*else if (this.viewMode !== "all" && e.key == "Home") {
  1722. e.preventDefault();
  1723. this.showFirst();
  1724. }
  1725. else if (this.viewMode !== "all" && e.key == "End") {
  1726. e.preventDefault();
  1727. this.showLast();
  1728. }*/
  1729. var scrollKeys = ["ArrowUp", "ArrowDown", " ", "PageUp", "PageDown", "Home", "End"]; //it turns out that scrolling the page is possible with stuff other than mouse wheel
  1730. if (scrollKeys.indexOf(e.key) >= 0) {
  1731. this.lastScrollTime = Date.now();
  1732. }
  1733. });
  1734. }
  1735.  
  1736. handleScroll() {
  1737. //check if the user scrolled to a different update on screen -> mark and save the position (only in whole thread view)
  1738. var lastUpdateAboveViewPort = null;
  1739. for (var key of this.elementsCache.keys()) {
  1740. var el = this.elementsCache.get(key);
  1741. if (el.offsetTop !== 0) {
  1742. if (el.offsetTop > document.defaultView.scrollY) {
  1743. break;
  1744. }
  1745. lastUpdateAboveViewPort = key;
  1746. }
  1747. }
  1748. var newUpdateIndex = lastUpdateAboveViewPort === null ? 0 : this.findUpdate(lastUpdateAboveViewPort);
  1749. if (this.currentUpdateIndex != newUpdateIndex) {
  1750. this.currentUpdateIndex = newUpdateIndex;
  1751. this.updateControls();
  1752. this.settingsChanged();
  1753. var updatesToExpand = [this.updates[this.currentUpdateIndex - 1], this.currentUpdate(), this.updates[this.currentUpdateIndex + 1]].filter(update => !!update);
  1754. updatesToExpand.forEach(update => this.expandUpdateImages(update));
  1755. }
  1756. }
  1757.  
  1758. modifyLayout() {
  1759. var op = this.elementsCache.get(this.threadID);
  1760. //change tab title to quest's title
  1761. var label = op.querySelector(".postwidth > label");
  1762. this.hasTitle = !!label.querySelector(".filetitle");
  1763. var title = label.querySelector(".filetitle") || label.querySelector(".postername");
  1764. title = title.textContent.trim();
  1765. document.title = title !== "Suggestion" ? title : "Untitled Quest";
  1766. //extend vertical size to prevent screen jumping when navigating updates
  1767. document.querySelector(".qrControlsTop").insertAdjacentHTML("beforeBegin", `<div class="haveOneScreenOfSpaceBelowHereSoItIsPossibleToScroll" />`);
  1768. //extend vertical size so it's possible to scroll to the last update in full thread view
  1769. this.elementsCache.get(this.lastUpdate().updatePostID).insertAdjacentHTML("afterBegin", `<div class="haveOneScreenOfSpaceBelowHereSoItIsPossibleToScroll" />`);
  1770. //prevent wrapping posts around the OP; setting clear:left on the 2nd post doesn't work because that element might be hidden
  1771. op.querySelector("blockquote").insertAdjacentHTML("afterEnd", `<div style="clear: left;"></div>`);
  1772. //prevent wrapping text underneath update images
  1773. this.updates.forEach(update => {
  1774. var updateEl = this.elementsCache.get(update.updatePostID);
  1775. if (update !== this.firstUpdate()) {
  1776. updateEl.querySelector(".reply, .highlight").classList.add("update");
  1777. }
  1778. });
  1779. //remove the "Report completed threads!" message from the top
  1780. var message = document.querySelector("body > center .filetitle");
  1781. if (message) {
  1782. message.classList.add("hidden");
  1783. }
  1784. var replyForm = document.forms.postform;
  1785. //remove the (Reply to #) text since it's obvious that we're replying to the thread that we're viewing, plus other text in the line
  1786.  
  1787. var replyToPostEl = replyForm.querySelector("#posttypeindicator");
  1788. if (replyToPostEl) {
  1789. [...replyToPostEl.parentElement.childNodes].filter(el => el && el.nodeType == HTMLElement.TEXT_NODE).forEach(el => el.remove());
  1790. replyToPostEl.remove();
  1791. }
  1792. var subjectEl = replyForm.querySelector(`input[name="subject"]`);
  1793. [...subjectEl.parentElement.childNodes].forEach(el => { if (el.nodeType == HTMLElement.TEXT_NODE) el.remove(); });
  1794. //move the upload file limitations info into a tooltip
  1795. var filetd = replyForm.querySelector(`input[type="file"]`);
  1796. var fileRulesEl = replyForm.querySelector("td.rules");
  1797. fileRulesEl.classList.add("hidden");
  1798. var fileRules = [...fileRulesEl.querySelectorAll("li")].splice(0, 2);
  1799. fileRules = fileRules.map(el => el.innerText.replace(new RegExp("[ \n]+", "g"), " ").trim()).join("\n");
  1800. filetd.insertAdjacentHTML("afterEnd", `&nbsp<span class="qrTooltip" title="${fileRules}">*</span>`);
  1801. //move the password help line into a tooltip
  1802. var postPasswordEl = replyForm.querySelector(`input[name="postpassword"]`);
  1803. postPasswordEl.nextSibling.remove();
  1804. postPasswordEl.insertAdjacentHTML("afterEnd", `&nbsp<span class="qrTooltip" title="Password for post and file deletion">?</span>`);
  1805. //reply form input placeholders
  1806. (replyForm.querySelector(`[name="name"]`) || {}).placeholder = "Name (leave empty for Suggestion)";
  1807. (replyForm.querySelector(`[name="em"]`) || {}).placeholder = "Options";
  1808. (replyForm.querySelector(`[name="subject"]`) || {}).placeholder = "Subject";
  1809. (replyForm.querySelector(`[name="message"]`) || {}).placeholder = "Message";
  1810. (replyForm.querySelector(`[name="embed"]`) || {}).placeholder = "Embed media";
  1811. //remove that annoying red strip
  1812. document.querySelector(".replymode").classList.add("hidden");
  1813. }
  1814.  
  1815. getTopControlsHtml() {
  1816. return `
  1817. <div class="qrControlsTop">
  1818. <div id="qrSettingsToggle">[<a href="#">Settings</a>]</div>
  1819. <div class="qrNavControls">
  1820. ${this.getNavControlsHtml()}
  1821. <span class="qrLinksTop">
  1822. ${this.getLinksHtml()}
  1823. </span>
  1824. </div>
  1825. ${this.getSettingsControlsHtml()}
  1826. <hr>
  1827. </div>
  1828. `;
  1829. }
  1830.  
  1831. getBottomControlsHtml() {
  1832. return `
  1833. <links class="qrLinksBottom">
  1834. ${this.getLinksHtml()}
  1835. </links>
  1836. <div class="qrNavControls">
  1837. ${this.getNavControlsHtml()}
  1838. </div>
  1839. <hr>
  1840. `;
  1841. }
  1842.  
  1843. getSettingsControlsHtml() {
  1844. return `
  1845. <div class="qrSettingsControls collapsedHeight">
  1846. <div class="qrSettingsPage">
  1847. <div style="grid-column-start: 2;">Viewing mode</div>
  1848. <select id="qrShowUpdatesDropdown" class="qrSettingsControl"><option value="all">Whole thread</option><option value="single">Paged per update</option><option value="sequence">Paged per sequence</option></select>
  1849. <div style="grid-column-start: 5;">Reply form</div>
  1850. <select id="qrReplyFormLocationDropdown" class="qrSettingsControl"><option value="top">At top</option><option value="bottom">At bottom</option></select>
  1851. <div style="grid-column-start: 2;">Show suggestions</div>
  1852. <select id="qrShowSuggestionsDropdown" class="qrSettingsControl"><option value="none">Never</option><option value="last">Last update only</option><option value="all">Always</option></select>
  1853. <div style="grid-column-start: 5;">Expand images</div>
  1854. <select id="qrExpandImagesDropdown" class="qrSettingsControl"><option value="none">Do not</option><option value="updates">For updates</option><option value="all">For all</option></select>
  1855. <div style="grid-column-start: 2;">Show author comments</div>
  1856. <select id="qrShowAuthorCommentsDropdown" class="qrSettingsControl"><option value="none">Never</option><option value="last">Last update only</option><option value="all">Always</option></select>
  1857. <div style="grid-column-start: 5;">Show update info</div>
  1858. <div><input type="checkbox" id="qrShowUpdateInfoCheckbox" class="qrSettingsControl"></div>
  1859. <div style="grid-column-start: 2;">Keyboard shortcuts</div>
  1860. <div><span class="qrSettingsControl qrTooltip">?<span class="qrTooltiptext">Left and Right arrow keys will <br>navigate between updates</span></span></div>
  1861. </div>
  1862. </div>
  1863. `;
  1864. }
  1865.  
  1866. getNavControlsHtml() {
  1867. return `
  1868. <span></span>
  1869. <span class="qrNavControl"><button class="qrNavButton" id="qrShowFirstButton" type="button">First</button></span>
  1870. <span class="qrNavControl"><button class="qrNavButton" id="qrShowPrevButton" type="button">Prev</button></span>
  1871. <span id="qrNavPosition" class="qrOutline" title="Index of the currently shown update slash the total number of updates">
  1872. <label class="qrCurrentPos">0</label> / <label class="qrTotalPos">0</label>
  1873. </span>
  1874. <span class="qrNavControl"><button class="qrNavButton" id="qrShowNextButton" type="button">Next</button></span>
  1875. <span class="qrNavControl"><button class="qrNavButton" id="qrShowLastButton" type="button">Last</button></span>
  1876. <span>
  1877. <span class="qrUpdateInfo qrOutline">
  1878. <label id="qrAuthorCommentsCount" title="# of author comment posts for the visible updates">A: 0</label>
  1879. <label id="qrSuggestionsCount" title="# of suggestion posts for the visible updates">S: 0</label>
  1880. <label id="qrSuggestersCount" title="# of unique suggesters for the visible updates">U: 0</label>
  1881. </span>
  1882. </span>
  1883. `;
  1884. }
  1885.  
  1886. getLinksHtml() {
  1887. return `
  1888. <span>[<a class="qrWikiLink" style="color: inherit" title="Link to the quest's wiki page (if available)">Wiki</a>]</span>
  1889. <span>[<a class="qrDisLink" style="color: inherit" title="Link to the quest's latest discussion thread">Discuss</a>]</span>
  1890. <span class="qrThreadsLinks" title="List of quest's threads">
  1891. <select id="qrThreadLinksDropdown">
  1892. <option value="thread1">Thread not found in wiki</option>
  1893. </select>
  1894. </span>
  1895. `;
  1896. }
  1897.  
  1898. getReplyFormHeaderHtml() {
  1899. return `
  1900. <thead id="qrReplyFormHeader">
  1901. <tr>
  1902. <th class="postblock">Reply form</th>
  1903. <th class="qrReplyFormButtons">
  1904. <span title="Minimize the Reply form">[&nbsp;<a id="qrReplyFormMinimize" href="#">_</a>&nbsp;]</span>
  1905. <span title="Pop out the Reply form and have it float">[<a id="qrReplyFormPopout" href="#"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor">
  1906. <path d="M928 128H352c-52.8 0-96 43.2-96 96v96H96c-52.8 0-96 43.2-96 96v384c0 52.8 43.2 96 96 96h576c52.8 0 96-43.2 96-96v-96h160c52.8 0 96-43.2
  1907. 96-96V224c0-52.8-43.198-96-96-96zM704 800c0 17.346-14.654 32-32 32H96c-17.346 0-32-14.654-32-32V416c0-17.346 14.654-32 32-32h160v224c0 52.8 43.2
  1908. 96 96 96h352v96z m256-192c0 17.346-14.654 32-32 32H352c-17.346 0-32-14.654-32-32V224c0-17.346 14.654-32 32-32h576c17.346 0 32 14.654 32 32v384z">
  1909. </path></svg></a>]</span>
  1910. </th>
  1911. </tr>
  1912. </thead>`;
  1913. }
  1914.  
  1915. getStylingHtml() {
  1916. var bgc = document.defaultView.getComputedStyle(document.body)["background-color"];
  1917. var fgc = document.defaultView.getComputedStyle(document.body).color;
  1918. return `
  1919. <style>
  1920. .hidden { display: none; }
  1921. .crimson { color: crimson; }
  1922. #qrShowFirstButton, #qrShowLastButton { width: 50px; }
  1923. #qrShowPrevButton, #qrShowNextButton { width: 100px; }
  1924. #qrSettingsToggle { position: absolute; left: 8px; padding-top: 2px; }
  1925. .qrControlsTop { }
  1926. .qrNavControls { display: grid; grid-template-columns: 1fr auto auto auto auto auto auto 1fr; grid-gap: 3px; color: ${fgc}; pointer-events: none; }
  1927. .qrNavControls > * { margin: auto 0px; pointer-events: all; }
  1928. .qrOutline { text-shadow: 2px 2px 2px ${bgc}, 2px 0px 2px ${bgc}, 2px -2px 2px ${bgc}, 0px -2px 2px ${bgc}, -2px -2px 2px ${bgc}, -2px 0px 2px ${bgc}, -2px 2px 2px ${bgc}, 0px 2px 2px ${bgc}, 0px 0px 2px ${bgc}; }
  1929. .qrLinksBottom { position: absolute; right: 8px; white-space: nowrap; color: ${fgc}; }
  1930. .qrLinksTop { white-space: nowrap; text-align: right; }
  1931. #qrNavPosition { font-weight: bold; white-space: nowrap; }
  1932. .qrUpdateInfo { white-space: nowrap; }
  1933. .qrSettingsControls { height: 84px; overflow: hidden; transition: all 0.3s; }
  1934. .qrSettingsPage { display: grid; grid-template-columns: 1fr auto auto 1fr auto auto 1fr; padding-top:4px; white-space: nowrap; }
  1935. .qrSettingsControl { margin-left: 4px; }
  1936. select.qrSettingsControl { width: 150px; }
  1937. #qrThreadLinksDropdown { max-width: 100px; }
  1938. .collapsedHeight { height: 0px; }
  1939. .qrTooltip { position: relative; border-bottom: 1px dotted; cursor: pointer; }
  1940. .qrTooltip:hover .qrTooltiptext { visibility: visible; }
  1941. .qrTooltip .qrTooltiptext { visibility: hidden; width: max-content; padding: 4px 4px 4px 10px; left: 15px; top: -35px;
  1942. position: absolute; border: dotted 1px; z-index: 1; background-color: ${bgc}; }
  1943. .haveOneScreenOfSpaceBelowHereSoItIsPossibleToScroll { position:absolute; height: 100vh; width: 1px; }
  1944. #qrReplyFormHeader { text-align: center; }
  1945. .postform td:first-child { display: none; }
  1946. .qrReplyFormButtons { position: absolute; right: 0px; }
  1947. .qrReplyFormButtons svg { width: 17px; vertical-align: bottom; }
  1948. .qrPopout { position: fixed; opacity: 0.2 !important; transition: opacity 0.3s; background-color: inherit; border: 1px solid rgba(0, 0, 0, 0.10); }
  1949. .qrPopout:hover { opacity: 1 !important; }
  1950. .qrPopout:focus-within { opacity: 1 !important; }
  1951. .qrPopout #qrReplyFormHeader { cursor: move; }
  1952. .stickyBottom { position: sticky; bottom: 0px; padding: 3px 0px; }
  1953. .update { width: 100%; }
  1954. .thumb:not([src$="spoiler.png"]) { width: unset; height: unset; max-width: calc(100% - 40px); max-height: calc(100vh - 100px); }
  1955. .userdelete { float:unset; position: absolute; right: 2px; }
  1956. body { position: relative; overflow-anchor: none; }
  1957. #watchedthreadlist { display: grid; grid-template-columns: auto auto 3fr auto 1fr auto auto 0px; color: transparent; }
  1958. #watchedthreadlist > a[href$=".html"] { grid-column-start: 1; }
  1959. #watchedthreadlist > a[href*="html#"] { max-width: 40px; }
  1960. #watchedthreadlist > * { margin: auto 0px; }
  1961. #watchedthreadlist > span { overflow: hidden; white-space: nowrap; }
  1962. #watchedthreadlist > .postername { grid-column-start: 5; }
  1963. #watchedthreadsbuttons { top: 0px; right: 0px; left: unset; bottom: unset; }
  1964. .reflinkpreview { z-index: 1; }
  1965. blockquote { margin-right: 1em; }
  1966. .postarea { background-color: inherit; }
  1967. .postform { position: relative; border-spacing: 0px; }
  1968. .postform :optional { box-sizing: border-box; }
  1969. .postform input[name="name"], .postform input[name="em"], .postform input[name="subject"] { width: 100% !important; }
  1970. .postform input[type="submit"] { position: absolute; right: 1px; bottom: 3px; }
  1971. .postform [name="imagefile"] { width: 220px; }
  1972. .postform td:first-child { display: none; }
  1973. #BLICKpreviewbut { margin-right: 57px; }
  1974. </style>
  1975. `;
  1976. /*
  1977. .qrNavControls { white-space: nowrap; text-align: center; pointer-events: none; background-color: transparent !important; }
  1978. .qrNavControls > * { pointer-events: initial; outline: 2px solid ${backgroundColor}; background-color: ${backgroundColor}; box-shadow: 0px 1px 0px 3px ${backgroundColor}; }
  1979. #qrNavPosition { display: inline-block; font-weight: bold; padding: 2px 6px 1px 6px; line-height: 1em; }
  1980. .qrUpdateInfo { position: absolute; right: 8px; padding-top: 2px; }
  1981. #watchedthreads { position: unset; margin: 2px 0px 2px 2px; float: right; height: unset !important; }
  1982. #watchedthreadsdraghandle { white-space: nowrap; overflow: hidden; }
  1983. #watchedthreadsbuttons { position: unset; }
  1984. #watchedthreadlist { display: grid; grid-template-columns: 40px 0px 0px 1fr auto 0px; overflow: hidden; color: transparent; }
  1985. #watchedthreadlist > * { margin: auto 0px; }
  1986. #watchedthreadlist > .filetitle { grid-column-start: 3; grid-column-end: 6; overflow: hidden; white-space: nowrap; }
  1987. #watchedthreadlist > .postername { grid-column-start: 3; white-space: nowrap; }
  1988. #watchedthreadlist > a[href*="html#"] { text-align: right; }
  1989. .logo { clear: unset; }
  1990. blockquote { clear: unset; }
  1991. .sentinel { position: absolute; left: 0px; top: 0px; width: 400px; pointer-events: none; background-color:white; opacity: 0.3; }
  1992. */
  1993. }
  1994. }
  1995.  
  1996. if (document.defaultView.QR) { //sanity check; don't run the script if it already ran
  1997. return;
  1998. }
  1999. if (document.defaultView.location.href.endsWith("+50.html") || document.defaultView.location.href.endsWith("+100.html")) {
  2000. return; //also, don't run the script when viewing partial thread
  2001. }
  2002.  
  2003. // for compatibility with certain other extensions, this extension runs last
  2004. setTimeout(() => {
  2005. var timeStart = Date.now();
  2006. // get settings from localStorage
  2007. var threadID = document.postform.replythread.value;
  2008. var lastThreadID = null;
  2009. var settings = document.defaultView.localStorage.getItem(`qrSettings${threadID}`);
  2010. if (!settings) {
  2011. lastThreadID = document.defaultView.localStorage.getItem("qrLastThreadID");
  2012. if (lastThreadID) {
  2013. settings = document.defaultView.localStorage.getItem(`qrSettings${lastThreadID}`);
  2014. if (settings) {
  2015. settings = JSON.parse(settings);
  2016. settings.currentUpdateIndex = 0;
  2017. }
  2018. }
  2019. }
  2020. else {
  2021. settings = JSON.parse(settings);
  2022. }
  2023. document.defaultView.QR = new QuestReader();
  2024. document.defaultView.QR.init(settings);
  2025. document.defaultView.QR.onSettingsChanged = (settings) => {
  2026. if (!lastThreadID || lastThreadID != threadID) {
  2027. document.defaultView.localStorage.setItem("qrLastThreadID", threadID.toString());
  2028. lastThreadID = threadID;
  2029. }
  2030. document.defaultView.localStorage.setItem(`qrSettings${threadID}`, JSON.stringify(settings));
  2031. };
  2032. console.log(`Quest Reader run time = ${Date.now() - timeStart}ms`);
  2033. }, 0);