您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Makes it more convenient to read quests
当前为
// ==UserScript== // @name Quest Reader // @author naileD // @namespace QuestReader // @include *//tgchan.org/kusaba/quest/res/* // @include *//tgchan.org/kusaba/questarch/res/* // @include *//tgchan.org/kusaba/graveyard/res/* // @description Makes it more convenient to read quests // @version 1 // @grant none // @icon data:image/vnd.microsoft.icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsSAAALEgHS3X78AAAANklEQVQokWNgoBOI2mJKpEomMvQgNAxRPUy4JGjjJJqoZoSrZmBgWOZzGlk/mlKILBMafxAAAE1pG/UEXzMMAAAAAElFTkSuQmCC // ==/UserScript== "use strict"; //entry point is more or less at the end of the script //enum const PostType = { UPDATE: 0, AUTHORCOMMENT: 1, SUGGESTION: 2, COMMENT: 3 } //UpdateAnalyzer class //Input: document of the quest //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. //Usage: var results = new UpdateAnalyzer().processQuest(document); class UpdateAnalyzer { constructor(options) { this.regex = UpdateAnalyzer.getRegexes(); if (options) { this.postCache = null; //Used to transfer posts cache to/from this class. Used for debugging purposes. this.useCache = options.useCache; //Used for debugging purposes. this.debug = options.debug; this.debugAfterDate = options.debugAfterDate; } } analyzeQuest(questDoc) { var posts = !this.postCache ? this.getPosts(questDoc) : JSON.parse(new TextDecoder().decode(this.postCache)); var authorID = posts[0].userID; //authodID is the userID of the first post this.threadID = posts[0].postID; //threadID is the postID of the first post this.totalFiles = this.getTotalFileCount(posts); 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 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); } var graphData = this.getUserGraphData(posts, questFixes, authorID); //get user names as nodes and edges for building user graph var users = this.buildUserGraph(graphData.nodes, graphData.edges); //build a basic user graph... whatever that means! this.author = this.find(users[authorID]); this.getUserPostAndFileCounts(posts, users, questFixes); //count the amount of posts and files each user made this.imageQuest = this.isImageQuest(questFixes); //image quest is when the author posts files at least 50% of the time if (this.debug) console.log(`Image quest: ${this.imageQuest}`); if (this.imageQuest) { //in case this is an image quest, merge users a bit differently users = this.buildUserGraph(graphData.nodes, graphData.edges, graphData.strongNodes, authorID); //build the user graph again, but with some restrictions this.author = this.find(users[authorID]); this.processFilePosts(posts, users, questFixes); //analyze file names and merge users based on when one file name is predicted from another this.getUserPostAndFileCounts(posts, users, questFixes); //count the amount of posts and files each user posted this.mergeCommonFilePosters(posts, users, questFixes); //merge certain file-posting users with the quest author this.mergeMajorityFilePoster(posts, users, questFixes); //consider a user who posted 50%+ of the files in the thread as the author } this.setPostUsers(posts, users, questFixes); //do final user resolution var postTypes = this.getFinalPostTypes(posts, questFixes); //determine which posts are updates return postTypes; } getPosts(questDoc) { var defaultName = "Suggestion"; var posts = {}; //dictionary => postID / post object questDoc.querySelectorAll(".postwidth").forEach(postHeaderElement => { //querySelectorAll is FASTER than getElementsByClassName when DOM is large var postID = parseInt(postHeaderElement.querySelector("span[id^=dnb]").id.split("-")[2]); if (posts[postID]) { //checking this may seem unnecessary, but it's required for compatibility with some imageboard scripts return; } var uid, name, trip, subject, fileElement, fileExt, fileName = "", activeContent, contentElement; var uidElement = postHeaderElement.querySelector(".uid"); uid = uidElement.textContent.substring(4); trip = postHeaderElement.querySelector(".postertrip"); if (trip) { //use tripcode instead of name if it exists name = trip.textContent; } else { name = postHeaderElement.querySelector(".postername").textContent.trim(); name = name == defaultName ? "" : name.toLowerCase(); } subject = postHeaderElement.querySelector(".filetitle"); subject = subject ? subject.textContent.trim() : ""; fileElement = postHeaderElement.querySelector(".filesize"); if (fileElement) { //try to get the original file name fileName = fileElement.getElementsByTagName("a")[0].href; var match = fileName.match(this.regex.fileExtension); fileExt = match ? match[0].toLowerCase() : ""; if (fileExt == ".png" || fileExt == ".gif" || fileExt == ".jpg" || fileExt == ".jpeg") { var fileInfo = fileElement.lastChild.textContent.split(", "); if (fileInfo.length >= 3) { fileName = fileInfo[2].split("\n")[0]; } } else { fileName = fileName.substr(fileName.lastIndexOf("/") + 1); //couldn't find original file name, use file name from the server instead } fileName = fileName.replace(this.regex.fileExtension, ""); //ignore file's extension } contentElement = postHeaderElement.nextElementSibling; activeContent = contentElement.querySelector("img, iframe") ? true : false; //does a post contain icons var postData = { postID: postID, userID: uid, userName: name, fileName: fileName, activeContent: activeContent }; if (this.useCache) { postData.textUpdate = this.regex.fraction.test(subject) || this.containsQuotes(contentElement); } else { postData.subject = subject; postData.contentElement = contentElement; } if (this.useCache || this.debug || this.debugAfterDate) { postData.date = Date.parse(postHeaderElement.querySelector("label").lastChild.nodeValue); } posts[postID] = postData; }); var postsArray = Object.values(posts); //convert to an array 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 this.postCache = new TextEncoder().encode(Object.toJSON ? Object.toJSON(postsArray) : JSON.stringify(postsArray)); //JSON.stringify stringifies twice. Another TGchan's protoaculous bug. } return postsArray; } getTotalFileCount(posts) { var totalFileCount = 0; posts.forEach(post => { if (post.fileName || post.activeContent) totalFileCount++; }); return totalFileCount; } isImageQuest(questFixes, ignore) { if (questFixes.imageQuest !== undefined) { return questFixes.imageQuest; } else { return (this.author.fileCount / this.author.postCount) >= 0.5; } } getUserGraphData(posts, questFixes, authorID) { var graphData = { nodes: new Set(), strongNodes: new Set(), edges: {} }; posts.forEach(post => { graphData.nodes.add(post.userID); if (post.userName) { graphData.nodes.add(post.userName); graphData.edges[`${post.userID}${post.userName}`] = { E1: post.userID, E2: post.userName }; } if (post.fileName || post.activeContent) { //strong nodes are user IDs that posted files graphData.strongNodes.add(post.userID); if (post.userName) { graphData.strongNodes.add(post.userName); } if (post.fileName && post.activeContent && post.userID != authorID) { //users that made posts with both file and icons are most likely the author graphData.edges[`${authorID}${post.userID}`] = { E1: authorID, E2: post.userID, hint: "fileAndIcons" }; } } }); for (var missedID in questFixes.missedAuthors) { //add missing links to the author from manual fixes graphData.edges[`${authorID}${missedID}`] = { E1: authorID, E2: missedID, hint: "missedAuthors" }; graphData.strongNodes.add(missedID); } graphData.edges = Object.values(graphData.edges); return graphData; } buildUserGraph(nodes, edges, strongNodes, authorID) { var users = {}; var edgesSet = new Set(edges); nodes.forEach(node => { users[node] = this.makeSet(node); }); if (!strongNodes) { edgesSet.forEach(edge => this.union(users[edge.E1], users[edge.E2])); } else { edgesSet.forEach(edge => { //merge strong with strong and weak with weak if ((strongNodes.has(edge.E1) && strongNodes.has(edge.E2)) || (!strongNodes.has(edge.E1) && !strongNodes.has(edge.E2))) { this.union(users[edge.E1], users[edge.E2]); edgesSet.delete(edge); } }); var author = this.find(users[authorID]); edgesSet.forEach(edge => { //merge strong with weak, but only for users which aren't the author if (this.find(users[edge.E1]) != author && this.find(users[edge.E2]) != author) { this.union(users[edge.E1], users[edge.E2]); } }); } return users; } processFilePosts(posts, users, questFixes) { var last2Files = new Map(); var filePosts = posts.filter(post => post.fileName && !questFixes.wrongImageUpdates[post.postID]); filePosts.forEach(post => { var postUser = this.find(users[post.userID]); var postFileName = post.fileName.match(this.regex.lastNumber) ? post.fileName : null; //no use processing files without numbers if (post.userName && this.find(users[post.userName]) == this.author) { postUser = this.author; } if (!last2Files.has(postUser)) { last2Files.set(postUser, [ null, null ]); } last2Files.get(postUser).shift(); last2Files.get(postUser).push(postFileName); last2Files.forEach((last2, user) => { if (user == postUser) { return; } if ((last2[0] !== null && this.fileNamePredicts(last2[0], post.fileName)) || (last2[1] !== null && this.fileNamePredicts(last2[1], post.fileName))) { if (this.debug || (this.debugAfterDate && this.debugAfterDate < post.date)) { 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})`); } var mergedUser = this.union(user, postUser); last2Files.delete(user.parent != user ? user : postUser); last2Files.get(mergedUser).shift(); last2Files.get(mergedUser).push(postFileName); if (this.find(this.author) == mergedUser) { this.author = mergedUser; } } }); }); return true; } getUserPostAndFileCounts(posts, users, questFixes) { for (var userID in users) { users[userID].postCount = 0; users[userID].fileCount = 0; } posts.forEach(post => { var user = this.decidePostUser(post, users, questFixes); user.postCount++; if (post.fileName || post.activeContent) { user.fileCount++; } }); } fileNamePredicts(fileName1, fileName2) { var match1 = fileName1.match(this.regex.lastNumber); var match2 = fileName2.match(this.regex.lastNumber); if (!match1 || !match2) { return false; } var indexDifference = match2.index - match1.index; if (indexDifference > 1 || indexDifference < -1) { return false; } var numberDifference = parseInt(match2[1]) - parseInt(match1[1]); if (numberDifference !== 2 && numberDifference !== 1) { return false; } var name1 = fileName1.replace(this.regex.lastNumber, ""); var name2 = fileName2.replace(this.regex.lastNumber, ""); return this.stringsAreSimilar(name1, name2); } stringsAreSimilar(string1, string2) { var lengthDiff = string1.length - string2.length; if (lengthDiff > 1 || lengthDiff < -1) { return false; } var s1 = lengthDiff > 0 ? string1 : string2; var s2 = lengthDiff > 0 ? string2 : string1; for (var i = 0, j = 0, diff = 0; i < s1.length; i++, j++) { if (s1[i] !== s2[j]) { diff++; if (diff === 2) { return false; } if (lengthDiff !== 0) { j--; } } } return true; } mergeMajorityFilePoster(posts, users, questFixes) { if (this.author.fileCount > this.totalFiles / 2) { return; } for (var userID in users) { if (users[userID].fileCount >= this.totalFiles / 2 && users[userID] != this.author) { if (this.debug || (this.debugAfterDate && this.debugAfterDate < posts[posts.length - 1].date)) { 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)}%`); } var parent = this.union(this.author, users[userID]); var child = users[userID].parent != users[userID] ? users[userID] : this.author; parent.fileCount += child.fileCount; parent.postCount += child.postCount; this.author = parent; return; } } } mergeCommonFilePosters(posts, users, questFixes) { var merged = []; var filteredUsers = Object.values(users).filter(user => user.parent == user && user.fileCount >= 3 && user.fileCount / user.postCount > 0.5 && user != this.author); var usersSet = new Set(filteredUsers); posts.forEach(post => { if ((post.fileName || post.activeContent) && !questFixes.wrongImageUpdates[post.postID] && this.isTextPostAnUpdate(post)) { for (var user of usersSet) { if (this.find(users[post.userID]) == user) { if (this.debug || (this.debugAfterDate && this.debugAfterDate < post.date)) { console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html new common poster ${users[post.userID].id}`); } var parent = this.union(this.author, user); var child = user.parent != user ? user : this.author; parent.fileCount += child.fileCount; parent.postCount += child.postCount; this.author = parent; usersSet.delete(user); break; } } } }); } setPostUsers(posts, users, questFixes) { posts.forEach(post => { post.user = this.decidePostUser(post, users, questFixes); }); } decidePostUser(post, users, questFixes) { var user = this.find(users[post.userID]); if (post.userName) { if (questFixes.ignoreTextPosts[post.userName]) { //choose to the one that isn't the author if (user == this.author) { user = this.find(users[post.userName]); } } else if (this.find(users[post.userName]) == this.author) { //choose the one that is the author user = this.author; } } return user; } getFinalPostTypes(posts, questFixes) { // Updates are posts made by the author and, in case of image quests, author posts that contain files or icons var postTypes = new Map(); posts.forEach(post => { var postType = PostType.SUGGESTION; if (post.user == this.author) { if (post.fileName || post.activeContent) { //image post if (!questFixes.wrongImageUpdates[post.postID]) { postType = PostType.UPDATE; } else if (!questFixes.ignoreTextPosts[post.userID] && !questFixes.ignoreTextPosts[post.userName]) { postType = PostType.AUTHORCOMMENT; } } else if (!questFixes.ignoreTextPosts[post.userID] && !questFixes.ignoreTextPosts[post.userName]) { //text post if (!questFixes.wrongTextUpdates[post.postID] && (!this.imageQuest || this.isTextPostAnUpdate(post))) { postType = PostType.UPDATE; } else { postType = PostType.AUTHORCOMMENT; } } if (questFixes.missedTextUpdates[post.postID]) { postType = PostType.UPDATE; } } if (this.debugAfterDate && this.debugAfterDate < post.date) { if (postType == PostType.SUGGESTION && post.fileName) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new non-update`); if (postType == PostType.AUTHORCOMMENT) console.log(`https://tgchan.org/kusaba/quest/res/${this.threadID}.html#${post.postID} new author comment`); 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`); } postTypes.set(post.postID, postType); }); return postTypes; } isTextPostAnUpdate(post) { if (post.textUpdate === undefined) { post.textUpdate = this.regex.fraction.test(post.subject) || this.containsQuotes(post.contentElement); } return post.textUpdate; } containsQuotes(contentElement) { //extract post's text, but ignore text inside spoilers, links, dice rolls or any sort of brackets var filteredContentText = ""; contentElement.childNodes.forEach(node => { if (node.className !== "spoiler" && node.nodeName != "A" && (node.nodeName != "B" || !this.regex.diceRoll.test(node.textContent))) { filteredContentText += node.textContent; } }); filteredContentText = filteredContentText.replace(this.regex.bracketedTexts, "").trim(); //if the post contains dialogue, then it's likely to be an update var quotedTexts = filteredContentText.match(this.regex.quotedTexts) || []; for (let q of quotedTexts) { if (this.regex.endsWithPunctuation.test(q)) { return true; } } return false; } makeSet(id) { var node = { id: id, children: [] }; node.parent = node; return node; } find(node) { //find with path halving while (node.parent != node) { var curr = node; node = node.parent; curr.parent = node.parent; } return node; } union(node1, node2) { var node1root = this.find(node1); var node2root = this.find(node2); if (node1root == node2root) { return node1root; } node2root.parent = node1root; node1root.children.push(node2root); //having a list of children isn't a part of Union-Find, but it makes debugging much easier node2root.children.forEach(child => node1root.children.push(child)); return node1root; } static getRegexes() { if (!this.regex) { //cache as a static class property this.regex = { fileExtension: new RegExp("[.][^.]+$"), //finds ".png" in "image.png" lastNumber: new RegExp("([0-9]+)(?=[^0-9]*$)"), //finds "50" in "image50.png" fraction: new RegExp("[0-9][ ]*/[ ]*[0-9]"), //finds "1/4" in "Update 1/4" diceRoll: new RegExp("^rolled [0-9].* = [0-9]+$"), //finds "rolled 10, 20 = 30" quotedTexts: new RegExp("[\"“”][^\"“”]*[\"“”]","gu"), //finds text inside quotes endsWithPunctuation: new RegExp("[.,!?][ ]*[\"“”]$"), //finds if a quote ends with a punctuation bracketedTexts: new RegExp("(\\([^)]*\\))|(\\[[^\\]]*\\])|(\\{[^}]*\\})|(<[^>]*>)", "gu"), //finds text within various kinds of brackets... looks funny canonID: new RegExp("^[0-9a-f]{6}$") }; } return this.regex; } getFixes(threadID) { var fixes = UpdateAnalyzer.getAllFixes()[threadID] || {}; //convert array values to lower case and then into object properties for faster access for (let prop of [ "missedAuthors", "missedTextUpdates", "wrongTextUpdates", "wrongImageUpdates", "ignoreTextPosts" ]) { if (!fixes[prop]) { fixes[prop] = { }; } else if (Array.isArray(fixes[prop])) { //can't use Array.reduce() because tgchan's js library protoaculous destroyed it fixes[prop] = fixes[prop].reduceRight((acc, el) => { if (!el.startsWith("!")) el = el.toLowerCase(); acc[el] = true; return acc; }, { }); } } return fixes; } // Manual fixes. In some cases it's simply impossible (impractical) to automatically determine which posts are updates. So we fix those rare cases manually. // list last updated on: // 2019/07/27 //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. //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. //(An empty ignoreTextPosts string matches posts with an empty/default poster name) //missedImageUpdates: Actually, no such fixes exist. All missed image update posts are added through adding author IDs to missedAuthors. //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. //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. //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. //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. //(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.) static getAllFixes() { if (!this.allFixes) { this.allFixes = { //cache as a static class property 12: { missedAuthors: [ "!g9Qfmdqho2" ] }, 26: { ignoreTextPosts: [ "Coriell", "!DHEj4YTg6g" ] }, 101: { wrongTextUpdates: [ "442" ] }, 171: { wrongTextUpdates: [ "1402" ] }, 504: { missedTextUpdates: [ "515", "597", "654", "1139", "1163", "1180", "7994", "9951" ] }, 998: { ignoreTextPosts: [ "" ] }, 1292: { missedAuthors: [ "Chaptermaster II" ], missedTextUpdates: [ "1311", "1315", "1318" ], ignoreTextPosts: [ "" ] }, 1702: { wrongImageUpdates: [ "2829" ] }, 3090: { ignoreTextPosts: [ "", "!94Ud9yTfxQ", "Glaive" ], wrongImageUpdates: [ "3511", "3574", "3588", "3591", "3603", "3612" ] }, 4602: { missedTextUpdates: [ "4630", "6375" ] }, 7173: { missedTextUpdates: [ "8515", "10326" ] }, 8906: { missedTextUpdates: [ "9002", "9009" ] }, 9190: { missedAuthors: [ "!OeZ2B20kbk" ], missedTextUpdates: [ "26073" ] }, 13595: { wrongTextUpdates: [ "18058" ] }, 16114: { missedTextUpdates: [ "20647" ] }, 17833: { ignoreTextPosts: [ "!swQABHZA/E" ] }, 19308: { missedTextUpdates: [ "19425", "19600", "19912" ] }, 19622: { wrongImageUpdates: [ "30710", "30719", "30732", "30765" ] }, 19932: { missedTextUpdates: [ "20038", "20094", "20173", "20252" ] }, 20501: { ignoreTextPosts: [ "bd2eec" ] }, 21601: { missedTextUpdates: [ "21629", "21639" ] }, 21853: { missedTextUpdates: [ "21892", "21898", "21925", "22261", "22266", "22710", "23308", "23321", "23862", "23864", "23900", "24206", "25479", "25497", "25943", "26453", "26787", "26799", "26807", "26929", "27328", "27392", "27648", "27766", "27809", "29107", "29145" ] }, 22208: { missedAuthors: [ "fb5d8e" ] }, 24530: { wrongImageUpdates: [ "25023" ] }, 25354: { imageQuest: false}, 26933: { missedTextUpdates: [ "26935", "26955", "26962", "26967", "26987", "27015", "28998" ] }, 29636: { missedTextUpdates: [ "29696", "29914", "30025", "30911" ], wrongImageUpdates: [ "30973", "32955", "33107" ] }, 30350: { imageQuest: false, wrongTextUpdates: [ "30595", "32354", "33704" ] }, 30357: { missedTextUpdates: [ "30470", "30486", "30490", "30512", "33512" ] }, 33329: { wrongTextUpdates: [ "43894" ] }, 37304: { ignoreTextPosts: [ "", "GREEN", "!!x2ZmLjZmyu", "Adept", "Cruxador", "!ifOCf11HXk" ] }, 37954: { missedTextUpdates: [ "41649" ] }, 38276: { ignoreTextPosts: [ "!ifOCf11HXk" ] }, 41510: { missedTextUpdates: [ "41550", "41746" ] }, 44240: { missedTextUpdates: [ "44324", "45768", "45770", "48680", "48687" ] }, 45522: { missedTextUpdates: [ "55885" ] }, 45986: { missedTextUpdates: [ "45994", "46019" ] }, 49306: { missedTextUpdates: [ "54246" ] }, 49400: { ignoreTextPosts: [ "!!IzZTIxBQH1" ] }, 49937: { missedTextUpdates: [ "52386" ] }, 53129: { wrongTextUpdates: [ "53505" ] }, 53585: { missedAuthors: [ "b1e366", "aba0a3", "18212a", "6756f8", "f98e0b", "1c48f4", "f4963f", "45afb1", "b94893", "135d9a" ], ignoreTextPosts: [ "", "!7BHo7QtR6I", "Test Pattern", "Rowan", "Insomnia", "!!L1ZwWyZzZ5" ] }, 54766: { missedAuthors: [ "e16ca8" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] }, 55639: { wrongImageUpdates: [ "56711", "56345", "56379", "56637" ] }, 56194: { wrongTextUpdates: [ "61608" ] }, 59263: { missedTextUpdates: [ "64631" ] }, 62091: { imageQuest: true}, 65742: { missedTextUpdates: [ "66329", "66392", "67033", "67168" ] }, 67058: { missedTextUpdates: [ "67191", "67685" ] }, 68065: { missedAuthors: [ "7452df", "1d8589" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] }, 70887: { missedAuthors: [ "e53955", "7c9cdd", "2084ff", "064d19", "51efff", "d3c8d2" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] }, 72794: { wrongTextUpdates: [ "76740" ] }, 74474: { missedAuthors: [ "309964" ] }, 75425: { missedTextUpdates: [ "75450", "75463", "75464", "75472", "75490", "75505", "77245" ] }, 75763: { missedAuthors: [ "068b0e" ], ignoreTextPosts: [ "!!IzZTIxBQH1" ] }, 76892: { missedTextUpdates: [ "86875", "86884", "87047", "88315" ] }, 79146: { missedAuthors: [ "4a3269" ] }, 79654: { missedTextUpdates: [ "83463", "83529" ] }, 79782: { missedTextUpdates: [ "79975", "80045" ] }, 82970: { missedTextUpdates: [ "84734" ] }, 83325: { missedAuthors: [ "076064" ] }, 84134: { imageQuest: false}, 85235: { missedTextUpdates: [ "85257", "85282", "113215", "114739", "151976", "152022", "159250" ] }, 88264: { missedAuthors: [ "3fec76", "714b9c" ] }, 92605: { ignoreTextPosts: [ "" ] }, 94645: { missedTextUpdates: [ "97352" ] }, 95242: { missedTextUpdates: [ "95263" ] }, 96023: { missedTextUpdates: [ "96242" ] }, 96466: { ignoreTextPosts: [ "Reverie" ] }, 96481: { imageQuest: true}, 97014: { missedTextUpdates: [ "97061", "97404", "97915", "98124", "98283", "98344", "98371", "98974", "98976", "98978", "99040", "99674", "99684" ] }, 99095: { wrongImageUpdates: [ "111452" ] }, 99132: { ignoreTextPosts: [ "" ] }, 100346: { missedTextUpdates: [ "100626", "100690", "100743", "100747", "101143", "101199", "101235", "101239" ] }, 101388: { ignoreTextPosts: [ "Glaive" ] }, 102433: { missedTextUpdates: [ "102519", "102559", "102758" ] }, 102899: { missedTextUpdates: [ "102903" ] }, 103435: { missedTextUpdates: [ "104279", "105950" ] }, 103850: { ignoreTextPosts: [ "" ] }, 106656: { wrongTextUpdates: [ "115606" ] }, 107789: { missedTextUpdates: [ "107810", "107849", "107899" ] }, 108599: { wrongImageUpdates: [ "171382", "172922", "174091", "180752", "180758" ] }, 108805: { wrongImageUpdates: [ "110203" ] }, 109071: { missedTextUpdates: [ "109417" ] }, 112133: { missedTextUpdates: [ "134867" ] }, 112414: { missedTextUpdates: [ "112455" ] }, 113768: { missedAuthors: [ "e9a4f7" ] }, 114133: { ignoreTextPosts: [ "" ] }, 115831: { missedTextUpdates: [ "115862" ] }, 119431: { ignoreTextPosts: [ "" ] }, 120384: { missedAuthors: [ "233aab" ] }, 126204: { imageQuest: true, missedTextUpdates: [ "127069", "127089", "161046", "161060", "161563" ] }, 126248: { missedTextUpdates: [ "193064" ] }, 128706: { missedAuthors: [ "2e2f06", "21b50e", "e0478c", "9c87f6", "931351", "e294f1", "749d64", "f3254a" ] }, 131255: { missedTextUpdates: [ "151218" ] }, 137683: { missedTextUpdates: [ "137723" ] }, 139086: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 139513: { missedTextUpdates: [ "139560" ] }, 141257: { missedTextUpdates: [ "141263", "141290", "141513", "146287" ], ignoreTextPosts: [ "" ], wrongImageUpdates: [ "141265" ] }, 146112: { missedAuthors: [ "//_emily" ] }, 153225: { missedTextUpdates: [ "153615", "153875" ] }, 155665: { missedTextUpdates: [ "155670", "155684", "155740" ] }, 156257: { missedTextUpdates: [ "156956" ] }, 157277: { missedAuthors: [ "23c8f1", "8bb533" ] }, 161117: { missedTextUpdates: [ "167255", "168000" ] }, 162089: { missedTextUpdates: [ "167940" ] }, 164793: { missedAuthors: [ "e973f4" ], ignoreTextPosts: [ "!TEEDashxDA" ] }, 165537: { missedAuthors: [ "a9f6ce" ] }, 173621: { ignoreTextPosts: [ "" ] }, 174398: { missedAuthors: [ "bf0d4e", "158c5c" ] }, 176965: { missedTextUpdates: [ "177012" ] }, 177281: { missedTextUpdates: [ "178846" ] }, 181790: { ignoreTextPosts: [ "Mister Brush" ], wrongImageUpdates: [ "182280" ] }, 183194: { ignoreTextPosts: [ "!CRITTerXzI" ], wrongImageUpdates: [ "183207" ] }, 183637: { imageQuest: false, wrongTextUpdates: [ "183736" ] }, 185345: { wrongTextUpdates: [ "185347" ] }, 185579: { missedTextUpdates: [ "188091", "188697", "188731", "188748", "190868" ] }, 186709: { missedTextUpdates: [ "186735" ] }, 188253: { missedTextUpdates: [ "215980", "215984", "222136" ] }, 188571: { missedTextUpdates: [ "188633" ] }, 188970: { ignoreTextPosts: [ "" ] }, 191328: { missedAuthors: [ "f54a9c", "862cf6", "af7d90", "4c1052", "e75bed", "09e145" ] }, 191976: { missedAuthors: [ "20fc85" ] }, 192879: { missedTextUpdates: [ "193009" ] }, 193934: { missedTextUpdates: [ "212768" ] }, 196310: { missedTextUpdates: [ "196401" ] }, 196517: { missedTextUpdates: [ "196733" ] }, 198458: { missedTextUpdates: [ "198505", "198601", "199570" ] }, 200054: { missedAuthors: [ "a4b4e3" ] }, 201427: { missedTextUpdates: [ "201467", "201844" ] }, 203072: { missedTextUpdates: [ "203082", "203100", "206309", "207033", "208766" ] }, 206945: { missedTextUpdates: [ "206950" ] }, 207011: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 207296: { missedTextUpdates: [ "214551" ] }, 207756: { missedTextUpdates: [ "208926" ] }, 209334: { missedTextUpdates: [ "209941" ] }, 210613: { missedTextUpdates: [ "215711", "220853" ] }, 210928: { missedTextUpdates: [ "215900" ] }, 211320: { ignoreTextPosts: [ "Kindling", "Bahu" ], wrongImageUpdates: [ "211587", "215436" ] }, 212584: { missedAuthors: [ "40a8d3" ] }, 212915: { missedTextUpdates: [ "229550" ] }, 217193: { missedAuthors: [ "7f1ecd", "c00244", "7c97d9", "8c0848", "491db1", "c2c011", "e15f89", "e31d52", "3ce5b4", "c1f2ce", "5f0943", "1dc978", "d65652", "446ab5", "f906a7", "dad664", "231806" ] }, 217269: { imageQuest: false, wrongTextUpdates: [ "217860", "219314" ] }, 218385: { missedAuthors: [ "812dcf" ] }, 220049: { ignoreTextPosts: [ "Slinkoboy" ], wrongImageUpdates: [ "228035", "337790" ] }, 222777: { imageQuest: false}, 224095: { missedTextUpdates: [ "224196", "224300", "224620", "244476" ] }, 233213: { missedTextUpdates: [ "233498" ], ignoreTextPosts: [ "Bahu" ] }, 234437: { missedTextUpdates: [ "234657" ] }, 237125: { missedTextUpdates: [ "237192" ] }, 237665: { imageQuest: true, ignoreTextPosts: [ "" ] }, 238281: { ignoreTextPosts: [ "TK" ] }, 238993: { missedTextUpdates: [ "239018", "239028", "239094" ] }, 240824: { imageQuest: false}, 241467: { missedTextUpdates: [ "241709" ] }, 242200: { missedTextUpdates: [ "246465", "246473", "246513" ] }, 242657: { missedAuthors: [ "2563d4" ] }, 244225: { missedTextUpdates: [ "245099", "245195", "245201" ] }, 244557: { missedTextUpdates: [ "244561" ], ignoreTextPosts: [ "" ] }, 244830: { missedAuthors: [ "e33093" ] }, 247108: { ignoreTextPosts: [ "Bahu" ], wrongImageUpdates: [ "258883", "265446" ] }, 247714: { missedTextUpdates: [ "247852" ] }, 248067: { ignoreTextPosts: [ "" ] }, 248856: { ignoreTextPosts: [ "" ] }, 248880: { imageQuest: true, ignoreTextPosts: [ "", "!qkgg.NzvRY", "!!EyA2IwLwVl", "!I10GFLsZCw", "!k6uRjGDgAQ", "Seven01a19" ] }, 251909: { missedTextUpdates: [ "255400" ] }, 252195: { missedTextUpdates: [ "260890" ] }, 252944: { missedAuthors: [ "Rizzie" ], ignoreTextPosts: [ "", "!!EyA2IwLwVl", "Seven01a19" ] }, 256339: { missedTextUpdates: [ "256359", "256379", "256404", "256440" ] }, 257726: { missedAuthors: [ "917cac" ] }, 258304: { missedTextUpdates: [ "269087" ] }, 261572: { imageQuest: false}, 261837: { missedAuthors: [ "14149d" ] }, 262128: { missedTextUpdates: [ "262166", "262219", "262455", "262500" ] }, 262574: { missedAuthors: [ "b7798b", "0b5a64", "687829", "446f39", "cc1ccd", "9d3d72", "72d5e4", "932db9", "4d7cb4", "9f327a", "940ab2", "a660d0" ], ignoreTextPosts: [ "" ] }, 263831: { imageQuest: false, wrongTextUpdates: [ "264063", "264716", "265111", "268733", "269012", "270598", "271254", "271852", "271855", "274776", "275128", "280425", "280812", "282417", "284354", "291231", "300074", "305150" ] }, 265656: { ignoreTextPosts: [ "Glaive17" ] }, 266542: { missedAuthors: [ "MidKnight", "c2c011", "f5e4b4", "e973f4", "6547ec" ], ignoreTextPosts: [ "", "!TEEDashxDA", "Not Cirr", "Ñ" ] }, 267348: { ignoreTextPosts: [ "" ] }, 269735: { ignoreTextPosts: [ "---" ] }, 270556: { ignoreTextPosts: [ "Bahu" ], wrongImageUpdates: [ "276022" ] }, 273047: { missedAuthors: [ "db463d", "16f0be", "77df62", "b6733e", "d171a3", "3a95e1", "21d450" ] }, 274088: { missedAuthors: [ "4b0cf3" ], missedTextUpdates: [ "294418" ], ignoreTextPosts: [ "" ] }, 274466: { missedAuthors: [ "c9efe3" ] }, 276562: { missedTextUpdates: [ "277108" ] }, 277371: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 278168: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 280381: { ignoreTextPosts: [ "!7BHo7QtR6I" ] }, 280985: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 283246: { imageQuest: false}, 285210: { ignoreTextPosts: [ "", "Weaver" ] }, 287296: { ignoreTextPosts: [ "", "Asplosionz" ] }, 287815: { missedAuthors: [ "Ñ" ] }, 288346: { missedAuthors: [ "383006", "bf1e7e" ], ignoreTextPosts: [ "383006", "bf1e7e" ] }, 289254: { imageQuest: false}, 292033: { wrongTextUpdates: [ "295088" ] }, 293532: { ignoreTextPosts: [ "" ] }, 294351: { ignoreTextPosts: [ "Weaver" ] }, 295374: { ignoreTextPosts: [ "TK" ] }, 295832: { missedAuthors: [ "ac22cd", "7afbc4", "6f11ff" ], missedTextUpdates: [ "313940" ] }, 295949: { missedTextUpdates: [ "296256", "297926", "298549" ] }, 298133: { missedTextUpdates: [ "298187" ] }, 298860: { imageQuest: true, missedTextUpdates: [ "298871", "298877", "298880", "298908" ] }, 299352: { imageQuest: true, missedTextUpdates: [ "299375", "299627", "303689" ] }, 300694: { ignoreTextPosts: [ "TK" ] }, 300751: { missedTextUpdates: [ "316287" ] }, 303859: { ignoreTextPosts: [ "" ] }, 308257: { missedTextUpdates: [ "314653" ] }, 309753: { missedTextUpdates: [ "309864", "309963", "310292", "310944", "310987", "311202", "311219", "311548" ] }, 310586: { missedTextUpdates: [ "310945", "312747", "313144" ] }, 311021: { missedAuthors: [ "049dfa", "f2a6f9" ] }, 312418: { missedTextUpdates: [ "312786", "312790", "312792", "312984", "313185" ] }, 314825: { ignoreTextPosts: [ "TK" ] }, 314940: { missedTextUpdates: [ "314986", "315198", "329923" ] }, 318478: { ignoreTextPosts: [ "Toxoglossa" ] }, 319491: { ignoreTextPosts: [ "Bahu" ] }, 323481: { missedTextUpdates: [ "323843", "324125", "324574" ] }, 323589: { missedTextUpdates: [ "329499" ] }, 327468: { missedTextUpdates: [ "327480", "337008" ] }, 337661: { ignoreTextPosts: [ "", "hisgooddog" ] }, 338579: { ignoreTextPosts: [ "", "Zealo8", "Ñ" ] }, 343078: { wrongImageUpdates: [ "343219" ] }, 343668: { missedTextUpdates: [ "343671" ] }, 348635: { ignoreTextPosts: [ "" ] }, 351064: { missedTextUpdates: [ "351634", "353263", "355326", "356289" ] }, 351264: { missedTextUpdates: [ "353077" ] }, 354201: { imageQuest: true, missedTextUpdates: [ "354340" ] }, 355404: { ignoreTextPosts: [ "Bahu" ] }, 356715: { missedTextUpdates: [ "356722" ] }, 357723: { missedAuthors: [ "7bad01" ], ignoreTextPosts: [ "", "SoqWizard" ] }, 359879: { imageQuest: false}, 359931: { missedAuthors: [ "Dasaki", "Rynh", "Kinasa", "178c80" ], ignoreTextPosts: [ "", "Gnoll", "Lost Planet", "Dasaki", "Slinkoboy" ] }, 360617: { missedAuthors: [ "7a7217" ] }, 363529: { imageQuest: true, ignoreTextPosts: [ "Tenyoken" ] }, 365082: { missedTextUpdates: [ "381411", "382388" ] }, 366944: { missedTextUpdates: [ "367897" ] }, 367145: { wrongTextUpdates: [ "367887" ] }, 367824: { missedTextUpdates: [ "367841", "367858", "367948" ] }, 375293: { ignoreTextPosts: [ "Bahu" ] }, 382864: { ignoreTextPosts: [ "FlynnMerk" ] }, 387602: { ignoreTextPosts: [ "!a1..dIzWW2" ], wrongImageUpdates: [ "390207", "392018", "394748" ] }, 388264: { ignoreTextPosts: [ "" ] }, 392034: { missedAuthors: [ "046f13" ] }, 392868: { missedAuthors: [ "e1359e" ] }, 393082: { ignoreTextPosts: [ "" ] }, 395700: { missedTextUpdates: [ "395701", "395758" ] }, 395817: { ignoreTextPosts: [ "" ] }, 397819: { ignoreTextPosts: [ "Bahu", "K-Dogg" ], wrongImageUpdates: [ "398064" ] }, 400842: { missedAuthors: [ "b0d466" ], ignoreTextPosts: [ "", "!a1..dIzWW2" ], wrongImageUpdates: [ "412172", "412197" ] }, 403418: { missedAuthors: [ "02cbc6" ] }, 404177: { missedTextUpdates: [ "404633" ] }, 409356: { missedTextUpdates: [ "480664", "485493" ], wrongTextUpdates: [ "492824" ] }, 410618: { ignoreTextPosts: [ "kathryn" ], wrongImageUpdates: [ "417836" ] }, 412463: { ignoreTextPosts: [ "" ] }, 413494: { ignoreTextPosts: [ "Bahu" ] }, 420600: { imageQuest: false}, 421477: { imageQuest: false}, 422052: { missedAuthors: [ "!a1..dIzWW2" ] }, 422087: { ignoreTextPosts: [ "Caz" ] }, 422856: { ignoreTextPosts: [ "", "???" ] }, 424198: { missedAuthors: [ "067a04" ], ignoreTextPosts: [ "!a1..dIzWW2" ] }, 425677: { missedTextUpdates: [ "425893", "426741", "431953" ] }, 426019: { ignoreTextPosts: [ "Taskuhecate" ] }, 427135: { ignoreTextPosts: [ "!7BHo7QtR6I" ] }, 427676: { ignoreTextPosts: [ "FRACTAL" ] }, 428027: { ignoreTextPosts: [ "notrottel", "Bahu", "!a1..dIzWW2", "Trout", "Larro", "", "cuoqet" ], wrongImageUpdates: [ "428285", "498295" ] }, 430036: { missedTextUpdates: [ "430062", "430182", "430416" ], ignoreTextPosts: [ "" ] }, 431445: { imageQuest: false, missedAuthors: [ "efbb86" ] }, 435947: { missedTextUpdates: [ "436059" ] }, 437675: { wrongTextUpdates: [ "445770", "449255", "480401" ] }, 437768: { missedTextUpdates: [ "446536" ] }, 438515: { ignoreTextPosts: [ "TK" ] }, 438670: { ignoreTextPosts: [ "" ] }, 441226: { missedAuthors: [ "6a1ec2", "99090a", "7f2d33" ], wrongImageUpdates: [ "441260" ] }, 441745: { missedTextUpdates: [ "443831" ] }, 447830: { imageQuest: false, missedAuthors: [ "fc985a", "f8b208" ], wrongTextUpdates: [ "448476", "450379", "452161" ] }, 448900: { missedAuthors: [ "0c2256" ] }, 449505: { wrongTextUpdates: [ "450499" ] }, 450563: { missedAuthors: [ "!!AwZwHkBGWx", "Oregano" ], ignoreTextPosts: [ "", "chirps", "!!AwZwHkBGWx", "!!AwZwHkBGWx", "Ham" ] }, 452871: { missedAuthors: [ "General Q. Waterbuffalo", "!cZFAmericA" ], missedTextUpdates: [ "456083" ] }, 453480: { ignoreTextPosts: [ "TK" ], wrongImageUpdates: [ "474233" ] }, 453978: { missedTextUpdates: [ "453986" ] }, 454256: { missedTextUpdates: [ "474914", "474957" ] }, 456185: { ignoreTextPosts: [ "TK" ], wrongTextUpdates: [ "472446" ], wrongImageUpdates: [ "592622" ] }, 456798: { missedTextUpdates: [ "516303" ] }, 458432: { missedAuthors: [ "259cce", "34cbef" ] }, 463595: { missedTextUpdates: [ "463711", "465024", "465212", "465633", "467107", "467286" ], wrongTextUpdates: [ "463623" ] }, 464038: { missedAuthors: [ "df885d", "8474cd" ] }, 465919: { missedTextUpdates: [ "465921" ] }, 469321: { missedTextUpdates: [ "469332" ] }, 471304: { missedAuthors: [ "1766db" ] }, 471394: { missedAuthors: [ "Cirr" ] }, 476554: { ignoreTextPosts: [ "Fish is yum" ] }, 478624: { missedAuthors: [ "88c9b2" ] }, 479712: { ignoreTextPosts: [ "" ] }, 481277: { missedTextUpdates: [ "481301", "482210" ], ignoreTextPosts: [ "Santova" ] }, 481491: { missedTextUpdates: [ "481543", "481575", "484069" ], ignoreTextPosts: [ "Zach Leigh", "Santova", "Outaki Shiba" ] }, 482391: { missedTextUpdates: [ "482501", "482838" ] }, 482629: { missedTextUpdates: [ "484220", "484437" ], ignoreTextPosts: [ "Santova", "Tera Nospis" ] }, 483108: { missedAuthors: [ "2de44c" ], missedTextUpdates: [ "483418", "483658" ], ignoreTextPosts: [ "Santova" ] }, 484423: { missedTextUpdates: [ "484470", "486761", "488602" ], ignoreTextPosts: [ "Tera Nospis", "Zach Leigh" ] }, 484606: { missedTextUpdates: [ "486773" ], ignoreTextPosts: [ "Zach Leigh" ] }, 485964: { missedTextUpdates: [ "489145", "489760" ], ignoreTextPosts: [ "Tera Nospis", "Santova" ] }, 489488: { missedTextUpdates: [ "490389" ] }, 489694: { missedAuthors: [ "2c8bbe", "30a140", "8c4b01", "8fbeb2", "2b7d97", "17675d", "782175", "665fcd", "e91794", "52019c", "8ef0aa", "e493a6", "c847bc" ] }, 489830: { missedAuthors: [ "9ee824", "8817a0", "d81bd3", "704658" ] }, 490689: { ignoreTextPosts: [ "Santova" ] }, 491171: { ignoreTextPosts: [ "Santova", "Zach Leigh", "Zack Leigh", "The Creator" ] }, 491314: { missedTextUpdates: [ "491498" ], ignoreTextPosts: [ "" ] }, 492511: { missedAuthors: [ "???" ] }, 493099: { ignoreTextPosts: [ "Zach Leigh", "Santova" ] }, 494015: { ignoreTextPosts: [ "Coda", "drgruff" ] }, 496561: { ignoreTextPosts: [ "Santova", "DJ LaLonde", "Tera Nospis" ] }, 498874: { ignoreTextPosts: [ "Santova" ] }, 499607: { ignoreTextPosts: [ "Santova", "Tera Nospis" ] }, 499980: { ignoreTextPosts: [ "Santova", "Tera Nospis", "DJ LaLonde" ] }, 500015: { missedTextUpdates: [ "500020", "500029", "500274", "501462", "501464", "501809", "505421" ], ignoreTextPosts: [ "suggestion", "Chelz" ] }, 502751: { ignoreTextPosts: [ "suggestion" ] }, 503053: { missedAuthors: [ "!!WzMJSzZzWx", "Shopkeep", "CAI" ] }, 505072: { missedTextUpdates: [ "565461" ] }, 505569: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 505633: { missedTextUpdates: [ "505694", "529582" ] }, 505796: { ignoreTextPosts: [ "Mister-Saturn" ] }, 506555: { ignoreTextPosts: [ "Tera Nospis", "Santova" ] }, 507761: { ignoreTextPosts: [ "", "Rue" ] }, 508294: { missedAuthors: [ "Lisila" ], missedTextUpdates: [ "508618", "508406" ] }, 509510: { missedTextUpdates: [ "509810", "510805", "510812", "510943", "511042", "512430", "514731", "515963" ] }, 510067: { missedTextUpdates: [ "510081" ] }, 511816: { imageQuest: true, missedAuthors: [ "34cf7d" ], missedTextUpdates: [ "512608" ] }, 512417: { ignoreTextPosts: [ "Uplifted" ] }, 512501: { ignoreTextPosts: [ "" ] }, 512569: { wrongImageUpdates: [ "512810" ] }, 513727: { missedTextUpdates: [ "519251" ], ignoreTextPosts: [ "!mYSM8eo.ng" ] }, 514174: { missedTextUpdates: [ "747164" ] }, 515255: { ignoreTextPosts: [ "" ] }, 516595: { imageQuest: true}, 517144: { ignoreTextPosts: [ "" ] }, 518737: { wrongTextUpdates: [ "521408", "522150", "522185", "522231", "535521" ] }, 518843: { ignoreTextPosts: [ "" ] }, 519463: { imageQuest: false}, 521196: { missedTextUpdates: [ "524608" ] }, 526472: { missedTextUpdates: [ "526524", "559848" ] }, 527296: { ignoreTextPosts: [ "Zealo8" ] }, 527546: { ignoreTextPosts: [ "suggestion" ] }, 527753: { missedAuthors: [ "7672c3", "9d78a6", "cb43c1" ] }, 528891: { ignoreTextPosts: [ "drgruff" ] }, 530940: { missedAuthors: [ "2027bb", "feafa5", "0a3b00" ] }, 533990: { missedTextUpdates: [ "537577" ] }, 534197: { ignoreTextPosts: [ "Stella" ] }, 535302: { ignoreTextPosts: [ "mermaid" ] }, 535783: { ignoreTextPosts: [ "drgruff" ] }, 536268: { missedTextUpdates: [ "536296", "538173" ], ignoreTextPosts: [ "Archivemod" ], wrongImageUpdates: [ "537996" ] }, 537343: { missedTextUpdates: [ "539218" ] }, 537647: { missedTextUpdates: [ "537683" ] }, 537867: { missedAuthors: [ "369097" ] }, 539831: { ignoreTextPosts: [ "" ] }, 540147: { ignoreTextPosts: [ "drgruff" ] }, 541026: { imageQuest: false}, 543428: { missedTextUpdates: [ "545458" ] }, 545071: { missedTextUpdates: [ "545081" ] }, 545791: { ignoreTextPosts: [ "" ] }, 545842: { missedTextUpdates: [ "550972" ] }, 548052: { missedTextUpdates: [ "548172" ], ignoreTextPosts: [ "Lucid" ] }, 548899: { missedTextUpdates: [ "548968", "549003" ] }, 549394: { missedTextUpdates: [ "549403" ] }, 553434: { missedTextUpdates: [ "553610", "553635", "553668", "554166" ] }, 553711: { missedTextUpdates: [ "553722", "553728", "554190" ] }, 553760: { missedTextUpdates: [ "554994", "555829", "556570", "556792", "556803", "556804" ] }, 554694: { missedTextUpdates: [ "557011", "560544" ] }, 556435: { missedAuthors: [ "Azathoth" ], missedTextUpdates: [ "607163" ], wrongTextUpdates: [ "561150" ] }, 557051: { missedTextUpdates: [ "557246", "557260", "557599", "559586" ], wrongTextUpdates: [ "557517" ] }, 557633: { imageQuest: true}, 557854: { missedTextUpdates: [ "557910", "557915", "557972", "558082", "558447", "558501", "561834", "561836", "562289", "632102", "632481", "632509", "632471" ] }, 562193: { ignoreTextPosts: [ "" ] }, 563459: { missedTextUpdates: [ "563582" ] }, 564852: { ignoreTextPosts: [ "Trout" ] }, 564860: { missedTextUpdates: [ "565391" ] }, 565909: { ignoreTextPosts: [ "" ] }, 567119: { missedTextUpdates: [ "573494", "586375" ] }, 567138: { missedAuthors: [ "4cf1b6" ] }, 568248: { missedTextUpdates: [ "569818" ] }, 568370: { ignoreTextPosts: [ "" ] }, 568463: { missedTextUpdates: [ "568470", "568473" ] }, 569225: { missedTextUpdates: [ "569289" ] }, 573815: { wrongTextUpdates: [ "575792" ] }, 578213: { missedTextUpdates: [ "578575" ] }, 581741: { missedTextUpdates: [ "581746" ] }, 582268: { missedTextUpdates: [ "587221" ] }, 585201: { ignoreTextPosts: [ "", "Bahustard", "Siphon" ] }, 586024: { ignoreTextPosts: [ "" ] }, 587086: { missedTextUpdates: [ "587245", "587284", "587443", "587454" ] }, 587562: { ignoreTextPosts: [ "Zealo8" ] }, 588902: { missedTextUpdates: [ "589033" ] }, 589725: { imageQuest: false}, 590502: { ignoreTextPosts: [ "" ], wrongTextUpdates: [ "590506" ] }, 590761: { missedTextUpdates: [ "590799" ], ignoreTextPosts: [ "" ] }, 591527: { missedTextUpdates: [ "591547", "591845" ] }, 592273: { imageQuest: false}, 592625: { wrongTextUpdates: [ "730228" ] }, 593047: { missedTextUpdates: [ "593065", "593067", "593068" ] }, 593899: { ignoreTextPosts: [ "mermaid" ] }, 595081: { ignoreTextPosts: [ "", "VoidWitchery" ] }, 595265: { imageQuest: false, wrongTextUpdates: [ "596676", "596717", "621360", "621452", "621466", "621469", "621503" ] }, 596262: { missedTextUpdates: [ "596291", "596611", "597910", "598043", "598145", "600718", "603311" ] }, 596345: { ignoreTextPosts: [ "mermaid" ] }, 596539: { missedTextUpdates: [ "596960", "596972", "596998", "597414", "614375", "614379", "614407", "616640", "668835", "668844", "668906", "668907", "668937", "668941", "669049", "669050", "669126", "671651" ], ignoreTextPosts: [ "pugbutt" ] }, 598767: { ignoreTextPosts: [ "FRACTAL" ] }, 602894: { ignoreTextPosts: [ "" ] }, 604604: { missedTextUpdates: [ "605127", "606702" ] }, 609653: { missedTextUpdates: [ "610108", "610137" ] }, 611369: { wrongImageUpdates: [ "620890" ] }, 611997: { missedTextUpdates: [ "612102", "612109" ], wrongTextUpdates: [ "617447" ] }, 613977: { missedTextUpdates: [ "614036" ] }, 615246: { missedTextUpdates: [ "638243", "638245", "638246", "638248" ] }, 615752: { ignoreTextPosts: [ "Uplifted" ] }, 617061: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 617484: { missedTextUpdates: [ "617509", "617830" ] }, 618712: { missedTextUpdates: [ "619097", "619821", "620260" ] }, 620830: { missedAuthors: [ "913f0d" ], ignoreTextPosts: [ "", "Sky-jaws" ] }, 623611: { ignoreTextPosts: [ "!5tTWT1eydY" ] }, 623897: { wrongTextUpdates: [ "625412" ] }, 625364: { missedTextUpdates: [ "635199" ] }, 625814: { missedAuthors: [ "330ce5", "f79974", "53688c", "a19cd5", "defceb" ], missedTextUpdates: [ "625990" ], ignoreTextPosts: [ "" ] }, 627139: { ignoreTextPosts: [ "", "Seal" ] }, 628023: { missedTextUpdates: [ "628323", "629276", "629668" ] }, 628357: { ignoreTextPosts: [ "" ] }, 632345: { ignoreTextPosts: [ "!TEEDashxDA" ] }, 632823: { missedTextUpdates: [ "632860", "633225", "633632", "633649", "633723", "634118" ], ignoreTextPosts: [ "" ] }, 633187: { missedTextUpdates: [ "633407", "633444", "634031", "634192", "634462" ] }, 633487: { missedAuthors: [ "8b8b34", "fe7a48", "20ca72", "668d91" ] }, 634122: { ignoreTextPosts: [ "Apollo" ] }, 639549: { ignoreTextPosts: [ "Apollo" ] }, 641286: { missedTextUpdates: [ "641650" ] }, 642667: { missedTextUpdates: [ "643113" ] }, 642726: { missedTextUpdates: [ "648209", "651723" ] }, 643327: { ignoreTextPosts: [ "" ] }, 644179: { missedTextUpdates: [ "647317" ] }, 645426: { missedTextUpdates: [ "651214", "670665", "671751", "672911", "674718", "684082" ] }, 648109: { missedTextUpdates: [ "711809", "711811" ] }, 648646: { missedTextUpdates: [ "648681" ] }, 651220: { missedTextUpdates: [ "653791" ] }, 651382: { missedAuthors: [ "bbfc3d" ] }, 651540: { missedTextUpdates: [ "651629" ] }, 655158: { ignoreTextPosts: [ "" ] }, 662096: { ignoreTextPosts: [ "" ] }, 662196: { missedAuthors: [ "Penelope" ], ignoreTextPosts: [ "", "Brom", "Wire" ] }, 662452: { ignoreTextPosts: [ "" ] }, 662661: { ignoreTextPosts: [ "" ] }, 663088: { missedAuthors: [ "f68a09", "8177e7" ], ignoreTextPosts: [ "", "!5tTWT1eydY", "Wire", "Brom", "Apollo", "Arhra" ] }, 663996: { missedTextUpdates: [ "673890" ] }, 668009: { missedTextUpdates: [ "668227" ] }, 668216: { imageQuest: false}, 669206: { imageQuest: true, missedAuthors: [ "75347e" ] }, 672060: { missedTextUpdates: [ "673216" ] }, 673444: { ignoreTextPosts: [ "" ] }, 673575: { missedAuthors: [ "a6f913", "3bc92d" ], ignoreTextPosts: [ "!5tTWT1eydY" ] }, 673811: { missedTextUpdates: [ "682275", "687221", "687395", "688995" ], ignoreTextPosts: [ "" ] }, 677271: { missedTextUpdates: [ "677384" ] }, 678114: { imageQuest: false}, 678608: { missedTextUpdates: [ "678789" ] }, 679357: { missedTextUpdates: [ "679359", "679983" ] }, 680125: { ignoreTextPosts: [ "", "BritishHat" ] }, 680206: { missedAuthors: [ "Gnuk" ] }, 681620: { missedAuthors: [ "d9faec" ] }, 683261: { missedAuthors: [ "3/8 MPP, 4/4 MF" ] }, 686590: { imageQuest: false}, 688371: { missedTextUpdates: [ "696249", "696257" ], ignoreTextPosts: [ "", "Chaos", "Ariadne", "Melinoe", "\"F\"ingGenius" ] }, 691136: { missedTextUpdates: [ "697620" ], ignoreTextPosts: [ "" ], wrongImageUpdates: [ "706696" ] }, 691255: { ignoreTextPosts: [ "" ] }, 692093: { missedAuthors: [ "Bergeek" ], ignoreTextPosts: [ "Boxdog" ] }, 692872: { missedTextUpdates: [ "717187" ] }, 693509: { missedAuthors: [ "640f86" ] }, 693648: { missedTextUpdates: [ "694655" ] }, 694230: { ignoreTextPosts: [ "" ] }, 700573: { missedTextUpdates: [ "702352", "720330" ], ignoreTextPosts: [ "" ] }, 701456: { ignoreTextPosts: [ "" ] }, 702865: { ignoreTextPosts: [ "" ] }, 705639: { wrongTextUpdates: [ "794696" ] }, 706303: { missedAuthors: [ "5a8006" ] }, 706439: { missedTextUpdates: [ "714791" ] }, 706938: { ignoreTextPosts: [ "" ] }, 711320: { missedTextUpdates: [ "720646", "724022" ] }, 712179: { missedTextUpdates: [ "712255", "715182" ] }, 712785: { ignoreTextPosts: [ "" ] }, 713042: { missedTextUpdates: [ "713704" ] }, 714130: { imageQuest: true}, 714290: { missedTextUpdates: [ "714307", "714311" ] }, 714858: { ignoreTextPosts: [ "" ] }, 715796: { ignoreTextPosts: [ "" ] }, 717114: { missedTextUpdates: [ "717454", "717628" ] }, 718797: { missedAuthors: [ "FRACTAL on the go" ] }, 718844: { missedAuthors: [ "kome", "Vik", "Friptag" ], missedTextUpdates: [ "721242" ] }, 719505: { ignoreTextPosts: [ "" ] }, 719579: { imageQuest: false}, 722585: { wrongTextUpdates: [ "724938" ] }, 726944: { ignoreTextPosts: [ "" ] }, 727356: { ignoreTextPosts: [ "" ] }, 727581: { missedTextUpdates: [ "728169" ] }, 727677: { ignoreTextPosts: [ "Melinoe" ] }, 728411: { missedTextUpdates: [ "728928" ] }, 730993: { missedTextUpdates: [ "731061" ] }, 732214: { imageQuest: true, wrongTextUpdates: [ "732277" ] }, 734610: { ignoreTextPosts: [ "D3w" ] }, 736484: { ignoreTextPosts: [ "Roman" ], wrongImageUpdates: [ "750212", "750213", "750214" ] }, 741609: { missedTextUpdates: [ "754524" ] }, 743976: { ignoreTextPosts: [ "", "Typo" ] }, 745694: { ignoreTextPosts: [ "Crunchysaurus" ] }, 750281: { ignoreTextPosts: [ "Autozero" ] }, 752572: { missedTextUpdates: [ "752651", "752802", "767190" ] }, 754415: { missedAuthors: [ "Apollo", "riotmode", "!0iuTMXQYY." ], ignoreTextPosts: [ "", "!5tTWT1eydY", "!0iuTMXQYY.", "Indonesian Gentleman" ] }, 755378: { missedAuthors: [ "!Ykw7p6s1S." ] }, 758668: { ignoreTextPosts: [ "LD" ] }, 767346: { ignoreTextPosts: [ "" ] }, 768858: { ignoreTextPosts: [ "LD" ] }, 774368: { missedTextUpdates: [ "774500" ] }, 774930: { missedTextUpdates: [ "794040" ] }, 778045: { missedTextUpdates: [ "778427", "779363" ] }, 779564: { ignoreTextPosts: [ "" ] }, 784068: { wrongTextUpdates: [ "785618" ] }, 785044: { wrongTextUpdates: [ "801329" ] }, 789976: { missedTextUpdates: [ "790596", "793934", "800875", "832472" ] }, 794320: { wrongTextUpdates: [ "795183" ] }, 798380: { missedTextUpdates: [ "799784", "800444", "800774", "800817", "801212" ] }, 799546: { missedTextUpdates: [ "801103", "802351", "802753" ] }, 799612: { missedTextUpdates: [ "799968", "801579" ] }, 800605: { missedAuthors: [ "Boris Calija", "3373e2", "2016eb", "a80028" ], ignoreTextPosts: [ "", "Boris Calija" ] }, 802411: { missedTextUpdates: [ "805002" ] }, 807972: { wrongTextUpdates: [ "811969" ] }, 809039: { wrongImageUpdates: [ "817508", "817511" ] }, 811957: { ignoreTextPosts: [ "via Discord" ] }, 814448: { missedTextUpdates: [ "817938" ] }, 817541: { missedAuthors: [ "Raptie" ] }, 822552: { imageQuest: false}, 823831: { missedAuthors: [ "Retro-LOPIS" ] }, 827264: { ignoreTextPosts: [ "LD", "DogFace" ] }, 830006: { missedAuthors: [ "Amaranth" ] }, 835062: { ignoreTextPosts: [ "Curves" ] }, 835750: { missedTextUpdates: [ "836870" ] }, 836521: { wrongTextUpdates: [ "848748" ] }, 837514: { ignoreTextPosts: [ "LD" ] }, 839906: { missedTextUpdates: [ "845724" ] }, 840029: { missedTextUpdates: [ "840044", "840543" ] }, 841851: { ignoreTextPosts: [ "Serpens", "Joy" ] }, 842392: { missedTextUpdates: [ "842434", "842504", "842544" ] }, 844537: { missedTextUpdates: [ "847326" ] }, 848887: { imageQuest: true, wrongTextUpdates: [ "851878" ] }, 854088: { missedTextUpdates: [ "860219" ], ignoreTextPosts: [ "Ursula" ] }, 854203: { ignoreTextPosts: [ "Zenthis" ] }, 857294: { wrongTextUpdates: [ "857818" ] }, 858913: { imageQuest: false}, 863241: { missedTextUpdates: [ "863519" ] }, 865754: { missedTextUpdates: [ "875371" ], ignoreTextPosts: [ "???" ] }, 869242: { ignoreTextPosts: [ "" ] }, 871667: { missedTextUpdates: [ "884575" ] }, 876808: { imageQuest: false}, 879456: { missedTextUpdates: [ "881847" ] }, 881097: { missedTextUpdates: [ "881292", "882339" ] }, 881374: { ignoreTextPosts: [ "LD" ] }, 885481: { imageQuest: false, wrongTextUpdates: [ "886892" ] }, 890023: { missedAuthors: [ "595acb" ] }, 897318: { missedTextUpdates: [ "897321", "897624" ] }, 897846: { missedTextUpdates: [ "897854", "897866" ] }, 898917: { missedAuthors: [ "Cee (mobile)" ] }, 900852: { missedTextUpdates: [ "900864" ] }, 904316: { missedTextUpdates: [ "904356", "904491" ] }, 907309: { missedTextUpdates: [ "907310" ] }, 913803: { ignoreTextPosts: [ "Typo" ] }, 915945: { missedTextUpdates: [ "916021" ] }, 917513: { missedTextUpdates: [ "917515" ] }, 918806: { missedTextUpdates: [ "935207" ] }, 921083: { ignoreTextPosts: [ "LawyerDog" ] }, 923174: { ignoreTextPosts: [ "Marn", "MarnMobile" ] }, 924317: { ignoreTextPosts: [ "" ] }, 926927: { missedTextUpdates: [ "928194" ] }, 929545: { missedTextUpdates: [ "929634" ] }, 930854: { missedTextUpdates: [ "932282" ] }, 934026: { missedTextUpdates: [ "934078", "934817" ] }, 935464: { missedTextUpdates: [ "935544", "935550", "935552", "935880" ] }, 939572: { missedTextUpdates: [ "940402" ] }, 1000012: { missedAuthors: [ "Happiness" ] } }; } return this.allFixes; } } //QuestReader class //Input: non //Output: none //Usage: var results = new UpdateAnalyzer().processQuest(document); class QuestReader { constructor() { this.updates = []; this.sequences = []; this.onSettingsChanged = null; var defaultSettings = this.getDefaultSettings(); this.currentUpdateIndex = defaultSettings.currentUpdateIndex; this.viewMode = defaultSettings.viewMode; this.showSuggestions = defaultSettings.showSuggestions; this.showAuthorComments = defaultSettings.showAuthorComments; this.expandImages = defaultSettings.expandImages; this.replyAtBottom = defaultSettings.replyAtBottom; } init(settings) { var updateAnalyzer = new UpdateAnalyzer(); var postTypes = updateAnalyzer.analyzeQuest(document); //run UpdateAnalyzer to determine which posts are updates and what not this.threadID = updateAnalyzer.threadID; this.updates = this.getUpdatePostGroups(postTypes); //organize posts into groups; 1 update per group this.sequences = this.getUpdateSequences(); //a list of unique update sequences; not really needed... this.insertControls(); //insert html elements for controls this.insertStyling(); //insert html elements for styling this.enableHotkeys(); //insert events to capture keys this.monitorUrlHashChanges(); //insert event to detect #hash change in the url; when clicking on a post link, automatically change the page to the one that has the post this.setSettings(this.validateSettings(settings)); //load settings this.refresh(true); //hide all posts and show only the relevant ones; enable/disable/update controls } getUpdatePostGroups(postTypes) { var updatePostGroups = []; var currentPostGroup = { updatePostID: 0, suggestions: [], authorComments: [] }; var postTypesArray = [...postTypes]; for (let i = postTypesArray.length - 1; i >= 0; i--) { if (postTypesArray[i][1] == PostType.UPDATE) { currentPostGroup.updatePostID = postTypesArray[i][0]; updatePostGroups.unshift(currentPostGroup); currentPostGroup = { updatePostID: 0, suggestions: [], authorComments: [] }; } else if (postTypesArray[i][1] == PostType.AUTHORCOMMENT) { currentPostGroup.authorComments.unshift(postTypesArray[i][0]); } else { currentPostGroup.suggestions.unshift(postTypesArray[i][0]); } } var currentUpdateSequence = []; updatePostGroups.forEach(postGroup => { currentUpdateSequence.push(postGroup); postGroup.sequence = currentUpdateSequence; if (postGroup.suggestions.length > 0) { currentUpdateSequence = []; } }); return updatePostGroups; } getUpdateSequences() { var sequences = []; this.updates.forEach(update => { if (update.sequence !== sequences[sequences.length - 1]) { sequences.push(update.sequence); } }); return sequences; } currentUpdate() { return this.updates[this.currentUpdateIndex]; } firstUpdate() { return this.updates[0]; } lastUpdate() { return this.updates[this.updates.length - 1]; } refresh(checkHash) { this.hideAll(); this.showCurrentUpdates(); this.updateControls(); var scrollToPostID = this.currentUpdate().updatePostID; if (checkHash && document.defaultView.location.hash) { scrollToPostID = document.defaultView.location.hash.replace("#", ""); this.currentUpdateIndex = this.findUpdate(scrollToPostID); } var el = scrollToPostID == this.threadID ? document.querySelector("body > form") : document.getElementById(`reply${scrollToPostID}`); var scrollOptions = { behavior: "smooth", block: this.viewMode == "all" ? "start" : "nearest" }; setTimeout(() => { el.scrollIntoView(scrollOptions); }, 0); //window.scroll({ top: el.offsetTop - 100, behavior: "smooth" }); //setTimeout(() => { document.querySelector(".qrControlsTop").scrollIntoView({behavior: "smooth", block: "nearest"}); }, 0); } hideAll() { document.getElementsByName(this.threadID.toString())[0].parentElement.parentElement.childElements().forEach(opPostChildEl => { if (opPostChildEl.className === "postwidth" || opPostChildEl.nodeName === "BLOCKQUOTE" || opPostChildEl.nodeName === "A" || opPostChildEl.className === "de-refmap") { opPostChildEl.style.display = "none"; } }); document.querySelectorAll("form table").forEach(tableEl => { if (!tableEl.className) { tableEl.style.display = "none"; } }); } findUpdate(postID) { for (var i = 0; i < this.updates.length; i++) { if (this.updates[i].updatePostID == postID || this.updates[i].suggestions.indexOf(postID) != -1 || this.updates[i].authorComments.indexOf(postID) != -1) { return i; } } } showCurrentUpdates() { if (this.viewMode == "sequence") { this.currentUpdate().sequence.forEach(update => this.showUpdate(update)); } else if (this.viewMode == "single") { this.showUpdate(this.currentUpdate()); } else { this.updates.forEach(update => this.showUpdate(update)); } } showUpdate(update) { this.showPost(update.updatePostID); if (this.showSuggestions) { update.suggestions.forEach(postID => this.showPost(postID)); } if (this.showAuthorComments) { update.authorComments.forEach(postID => this.showPost(postID)); } } showPost(postID) { if (postID == this.threadID) { document.getElementsByName(postID.toString())[0].parentElement.parentElement.childElements().forEach(node => { if (node.className === "postwidth" || node.nodeName === "BLOCKQUOTE") { node.style.display = ""; } }); } else { var node = document.getElementById(`reply${postID}`).parentElement.parentElement.parentElement; node.style.display = ""; } if (this.expandImages) { this.expandImage(postID); } } showFirst() { this.currentUpdateIndex = 0; this.refresh(); this.settingsChanged(); } showLast() { this.currentUpdateIndex = this.updates.length - 1; this.refresh(); this.settingsChanged(); } showNext() { if (this.viewMode == "sequence") { var currentUpdateSequence = this.currentUpdate().sequence; while (this.currentUpdateIndex < this.updates.length - 1 && this.updates[this.currentUpdateIndex].sequence == currentUpdateSequence) { this.currentUpdateIndex++; } if (this.currentUpdate().sequence == this.lastUpdate().sequence) { // in case the user switches to single update view and this is the last sequence, he should be met with the last update this.currentUpdateIndex = this.updates.length - 1; } } else if (this.currentUpdateIndex < this.updates.length - 1) { this.currentUpdateIndex++; } this.refresh(); this.settingsChanged(); } showPrevious() { if (this.viewMode == "sequence") { var currentUpdateSequence = this.currentUpdate().sequence; while (this.currentUpdateIndex > 0 && this.updates[this.currentUpdateIndex].sequence == currentUpdateSequence) { this.currentUpdateIndex--; } if (this.currentUpdate().sequence == this.firstUpdate().sequence) { // in case the user switches to single update view and this is the first sequence, he should be met with the first update this.currentUpdateIndex = 0; } } else if (this.currentUpdateIndex > 0) { this.currentUpdateIndex--; } this.refresh(); this.settingsChanged(); } setSettings(settings) { if (settings) { for(var settingName in settings) { this[settingName] = settings[settingName]; } } } validateSettings(settings) { if (!settings) { return settings; } if (settings.currentUpdateIndex < 0) settings.currentUpdateIndex = 0; if (settings.currentUpdateIndex >= this.updates.length) settings.currentUpdateIndex = this.updates.length - 1; return settings; } settingsChanged() { if (this.onSettingsChanged) { var settings = { currentUpdateIndex: this.currentUpdateIndex, viewMode: this.viewMode, showSuggestions: this.showSuggestions, showAuthorComments: this.showAuthorComments, expandImages: this.expandImages, replyAtBottom: this.replyAtBottom, } this.onSettingsChanged(settings); } } getDefaultSettings() { return { currentUpdateIndex: 0, viewMode: "sequence", showSuggestions: true, showAuthorComments: true, expandImages: false, replyAtBottom: true, }; } toggleSettingsControls() { event.preventDefault(); //prevent scrolling to the top when clicking the link var settingsEl = document.querySelector(".qrSettingsControls"); settingsEl.classList.toggle("hidden"); var label = event.target; label.text = settingsEl.classList.contains("hidden") ? "Settings" : "Hide Settings"; } updateSettings() { this.viewMode = document.getElementById("qrShowUpdatesDropdown").value; this.showSuggestions = document.getElementById("qrShowSuggestionsCheckbox").checked === true; this.showAuthorComments = document.getElementById("qrShowAuthorCommentsCheckbox").checked === true; this.expandImages = document.getElementById("qrExpandImagesCheckbox").checked === true; this.replyAtBottom = document.getElementById("qrReplyFormAtBottom").checked === true; this.refresh(); this.settingsChanged(); } updateControls() { var leftDisabled = true; var rightDisabled = true; var current = 1; var last = 1; var suggestionsCount; var authorCommentsCount; if (this.viewMode == "sequence") { leftDisabled = this.currentUpdate().sequence == this.firstUpdate().sequence; rightDisabled = this.currentUpdate().sequence == this.lastUpdate().sequence; current = this.sequences.indexOf(this.currentUpdate().sequence) + 1; last = this.sequences.length; suggestionsCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].suggestions.length; authorCommentsCount = this.currentUpdate().sequence[this.currentUpdate().sequence.length - 1].authorComments.length; } else { leftDisabled = this.currentUpdate() == this.firstUpdate(); rightDisabled = this.currentUpdate() == this.lastUpdate(); current = this.currentUpdateIndex + 1; last = this.updates.length; if (this.viewMode == "single") { suggestionsCount = this.currentUpdate().suggestions.length; authorCommentsCount = this.currentUpdate().authorComments.length; } else { suggestionsCount = this.updates.reduceRight((sum, update) => { sum = sum + update.suggestions.length; return sum; }, 0); authorCommentsCount = this.updates.reduceRight((sum, update) => { sum = sum + update.authorComments.length; return sum; }, 0); } } // buttons document.querySelectorAll("#qrShowFirstButton, #qrShowPrevButton").forEach(button => { button.disabled = leftDisabled; }); document.querySelectorAll("#qrShowNextButton, #qrShowLastButton").forEach(button => { button.disabled = rightDisabled; }); // update info document.querySelectorAll("#qrNavPosition").forEach(label => { label.textContent = `${current} / ${last}`; }); document.querySelectorAll("#qrSuggestionsCount").forEach(label => { label.textContent = `S:${suggestionsCount}`; }); document.querySelectorAll("#qrAuthorCommentsCount").forEach(label => { label.textContent = ` A:${authorCommentsCount}`; }); // settings document.getElementById("qrShowUpdatesDropdown").value = this.viewMode; document.getElementById("qrShowSuggestionsCheckbox").checked = this.showSuggestions; document.getElementById("qrShowAuthorCommentsCheckbox").checked = this.showAuthorComments; document.getElementById("qrExpandImagesCheckbox").checked = this.expandImages; document.getElementById("qrReplyFormAtBottom").checked = this.replyAtBottom; // sticky controls when viewing whole thread var controlsContainer = document.querySelector(".qrControlsTop"); controlsContainer.classList.toggle("sticky", this.viewMode == "all"); // reply form at bottom var postarea = document.querySelector(".postarea"); var replymode = document.querySelector(".replymode"); var isReplyFormAtTop = (replymode == postarea.previousElementSibling); replymode.style.display = "none"; if (this.replyAtBottom && isReplyFormAtTop) { //move it down postarea.remove(); document.body.insertBefore(postarea, document.querySelectorAll(".navbar")[1]) controlsContainer.insertAdjacentHTML("beforeBegin", "<hr>"); } else if (!this.replyAtBottom && !isReplyFormAtTop) { //move it up postarea.remove(); replymode.insertAdjacentElement("afterEnd", postarea); controlsContainer.previousElementSibling.remove(); //remove <hr> } } insertControls() { document.querySelector("body > form").insertAdjacentHTML("beforebegin", this.getTopControlsHtml()); document.querySelector(".userdelete").insertAdjacentHTML("beforebegin", this.getNavControlsHtml() + "<hr>"); } insertStyling() { document.body.insertAdjacentHTML("beforeend", this.getStylingHtml()); } enableHotkeys() { document.addEventListener("keydown", (e) => { var inputTypes = ['text', 'password', 'number', 'email', 'tel', 'url', 'search', 'date', 'datetime', 'datetime-local', 'time', 'month', 'week'] if (e.target.tagName === "TEXTAREA" || (e.target.tagName === "INPUT" && inputTypes.indexOf(e.target.type) >= 0)) { return; //prevent our keyboard shortcuts when focused on a text input field } if (e.key == "ArrowRight") { this.showNext(); } else if (e.key == "ArrowLeft") { this.showPrevious(); } }); } monitorUrlHashChanges() { document.defaultView.onhashchange = (e) => { if (document.defaultView.location.hash) { this.refresh(true); } } } expandImage(postID) { var el = document.getElementById(`thumb${postID}`); if (!el) return; var link = el.parentElement.parentElement.querySelector(".filesize > a"); var img = el.children[0]; if (img.src !== link.href) { link.click(); } } getTopControlsHtml() { return ` <div class="qrControlsTop"> ${this.getSettingsControlsHtml()} <label class="qrSettingsLabel">[<a href="#" id="qrShowSettings" onclick="QR.toggleSettingsControls()">Settings</a>]</label> ${this.getNavControlsHtml()} <hr> </div> `; } getSettingsControlsHtml() { return ` <span class="qrSettingsControls hidden"> <span> <div>Viewing mode:</div> <div>Show suggestions:</div> <div>Show author comments:</div> <div>Expand images:</div> <div>Reply form at bottom:</div> <div>Keyboard shortcuts:</div> </span> <span> <div> <select id="qrShowUpdatesDropdown" class="qrSettingsControl" onchange="QR.updateSettings()"> <option value="all">Whole thread</option> <option value="single">Paged per update</option> <option value="sequence">Paged per update sequence</option> </select> </div> <div><input type="checkbox" id="qrShowSuggestionsCheckbox" class="qrSettingsControl" onclick="QR.updateSettings()"></div> <div><input type="checkbox" id="qrShowAuthorCommentsCheckbox" class="qrSettingsControl" onclick="QR.updateSettings()"></div> <div><input type="checkbox" id="qrExpandImagesCheckbox" class="qrSettingsControl" onclick="QR.updateSettings()"></div> <div><input type="checkbox" id="qrReplyFormAtBottom" class="qrSettingsControl" onclick="QR.updateSettings()"></div> <div><span id="hotkeysTooltip" class="qrSettingsControl tooltip">?<span class="tooltiptext">Left and Right arrow keys will navigate between updates in any mode.</span></span></div> </span> </span> `; } getNavControlsHtml() { return ` <div class="qrNavControls"> <span class="qrNavControl"><button class="qrNavButton" id="qrShowFirstButton" type="button" onclick="QR.showFirst()">First</button></span> <span class="qrNavControl"><button class="qrNavButton" id="qrShowPrevButton" type="button" onclick="QR.showPrevious()">Prev</button></span> <label id="qrNavPosition" title="Index of the currently shown update slash the total number of updates.">0 / 0</label> <span class="qrNavControl"><button class="qrNavButton" id="qrShowNextButton" type="button" onclick="QR.showNext()">Next</button></span> <span class="qrNavControl"><button class="qrNavButton" id="qrShowLastButton" type="button" onclick="QR.showLast()">Last</button></span> <div class="qrUpdateInfo"><label id="qrSuggestionsCount" title="Number of suggestion posts this update has.">S: 0</label><label id="qrAuthorCommentsCount" title="Number of author comment posts this update has.">A: 0</label></div> </div> `; } getStylingHtml() { return ` <style> #qrShowFirstButton, #qrShowLastButton { width: 50px; } #qrShowPrevButton, #qrShowNextButton { width: 100px; } .qrControlsTop { position: relative; } .qrSettingsControls > span { display: inline-block; vertical-align: top; text-align: start; } .qrSettingsControls > span > div { height: 20px; } .qrSettingsControl { margin-left: 4px } .qrSettingsLabel { position: absolute; left: 0px; z-index: 1; } .qrNavControls { white-space: nowrap; text-align: center; position: relative; } .qrUpdateInfo { position: absolute; right: 0px; top: 2px; } #hotkeysTooltip { border-bottom: 1px dotted; cursor: pointer; padding: 0px 2px 0px 2px; } .hidden { display:none; } .sticky { position: sticky; top: 0px; background-color: inherit; } .tooltip { position: relative; display: inline-block; } .tooltip:hover .tooltiptext { visibility: visible; } .tooltip .tooltiptext { visibility: hidden; width: 225px; text-align: center; padding: 4px; left: 20px; position: absolute; border: dotted 1px; z-index: 1; background-color: ${document.defaultView.getComputedStyle(document.body)["background-color"]}; } .thumb { width: unset; height: unset; max-width: ${100 - (40 / (document.defaultView.innerWidth / 100))}%; max-height: ${100 - (100 / (document.defaultView.innerHeight / 100))}vh; } </style> `; } } if (window.QR) { //sanity check; don't run the script if it already ran return; } if (window.location.href.endsWith("+50.html") || window.location.href.endsWith("+100.html")) { return; //also, don't run the script when viewing partial thread } // for compatibility with certain other extensions, this extension runs last setTimeout(() => { var timeStart = Date.now(); // get settings from localStorage var threadID = document.postform.replythread.value; var lastThreadID = null; var settings = window.localStorage.getItem(`qrSettings${threadID}`); if (!settings) { lastThreadID = window.localStorage.getItem("qrLastThreadID"); if (lastThreadID) { settings = window.localStorage.getItem(`qrSettings${lastThreadID}`); if (settings) { settings = JSON.parse(settings); settings.currentUpdateIndex = 0; } } } else { settings = JSON.parse(settings); } window.QR = new QuestReader(); window.QR.init(settings); window.QR.onSettingsChanged = (settings) => { if (!lastThreadID || lastThreadID != threadID) { window.localStorage.setItem("qrLastThreadID", threadID.toString()); lastThreadID = threadID; } window.localStorage.setItem(`qrSettings${threadID}`, JSON.stringify(settings)); } console.log(`Quest Reader run time = ${Date.now() - timeStart}ms`); }, 0);